package cli import ( "bytes" "context" "strings" "testing" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/app" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) func TestRunHelpLongFlag(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer err := Run(context.Background(), []string{"--help"}, &stdout, &stderr) if err != nil { t.Fatalf("Run() error = %v", err) } if !strings.Contains(stdout.String(), "generate daily") { t.Fatalf("help output missing generate command:\n%s", stdout.String()) } } func TestRunHelpShortFlag(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer err := Run(context.Background(), []string{"-h"}, &stdout, &stderr) if err != nil { t.Fatalf("Run() error = %v", err) } if !strings.Contains(stdout.String(), "weatherreporter run evening") { t.Fatalf("help output missing run command:\n%s", stdout.String()) } } func TestRunUnknownCommand(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer err := Run(context.Background(), []string{"inspect"}, &stdout, &stderr) if err == nil { t.Fatal("Run() error = nil, want unknown command error") } if !strings.Contains(err.Error(), `unknown command "inspect"`) { t.Fatalf("Run() error = %q, want unknown command message", err.Error()) } } func TestRunGenerateReturnsNotImplementedAfterResolution(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer runner := Runner{Clock: fixedClock()} err := runner.Run(context.Background(), []string{"generate", "tomorrow", "--units", "metric"}, &stdout, &stderr) if err == nil { t.Fatal("Run() error = nil, want app not implemented error") } if !strings.Contains(err.Error(), "generate is not implemented") { t.Fatalf("Run() error = %q, want app not implemented error", err.Error()) } } func TestResolveGenerateCommands(t *testing.T) { runner := Runner{Clock: fixedClock()} tests := []struct { name string args []string want app.ReportKind }{ {name: "daily", args: []string{"daily", "--date", "2026-05-29"}, want: app.ReportDaily}, {name: "tomorrow", args: []string{"tomorrow"}, want: app.ReportTomorrow}, {name: "three-day", args: []string{"three-day"}, want: app.ReportThreeDay}, {name: "weekend", args: []string{"weekend"}, want: app.ReportWeekend}, {name: "storm", args: []string{"storm", "--start", "2026-05-29T18:00", "--end", "2026-05-30T06:00"}, want: app.ReportStorm}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req, err := runner.resolveGenerate(tt.args) if err != nil { t.Fatalf("resolveGenerate() error = %v", err) } if req.Report != tt.want { t.Fatalf("Report = %q, want %q", req.Report, tt.want) } }) } } func TestResolveGenerateDailyDefaultsDateInConfiguredTimezone(t *testing.T) { runner := Runner{Clock: fixedClock()} req, err := runner.resolveGenerate([]string{"daily"}) if err != nil { t.Fatalf("resolveGenerate() error = %v", err) } if got := req.Date.Format(timeutil.DateLayout); got != "2026-05-29" { t.Fatalf("Date = %s, want 2026-05-29", got) } } func TestResolveGenerateAppliesSharedFlags(t *testing.T) { runner := Runner{Clock: fixedClock()} req, err := runner.resolveGenerate([]string{"daily", "--units", "metric", "--tz", "UTC", "--out", "./daily.md"}) if err != nil { t.Fatalf("resolveGenerate() error = %v", err) } if req.Config.WeatherAPI.Units != "metric" { t.Fatalf("Units = %q, want metric", req.Config.WeatherAPI.Units) } if req.Config.WeatherAPI.Timezone != "UTC" { t.Fatalf("Timezone = %q, want UTC", req.Config.WeatherAPI.Timezone) } if req.OutputPath != "./daily.md" { t.Fatalf("OutputPath = %q, want ./daily.md", req.OutputPath) } } func TestResolveGenerateStormRequiresStartAndEnd(t *testing.T) { runner := Runner{Clock: fixedClock()} _, err := runner.resolveGenerate([]string{"storm", "--start", "2026-05-29T18:00"}) if err == nil { t.Fatal("resolveGenerate() error = nil, want missing end error") } if !strings.Contains(err.Error(), "requires --end") { t.Fatalf("error = %q, want missing end", err.Error()) } } func TestResolveGenerateStormParsesRFC3339(t *testing.T) { runner := Runner{Clock: fixedClock()} req, err := runner.resolveGenerate([]string{ "storm", "--start", "2026-05-29T18:00:00-05:00", "--end", "2026-05-30T06:00:00-05:00", }) if err != nil { t.Fatalf("resolveGenerate() error = %v", err) } if !req.StormEnd.After(req.StormStart) { t.Fatalf("StormEnd = %s, want after %s", req.StormEnd, req.StormStart) } } func TestResolveRunCommands(t *testing.T) { tests := []struct { name string args []string want app.BatchKind }{ {name: "morning", args: []string{"morning"}, want: app.BatchMorning}, {name: "evening", args: []string{"evening", "--tz", "UTC"}, want: app.BatchEvening}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req, err := resolveRun(tt.args) if err != nil { t.Fatalf("resolveRun() error = %v", err) } if req.Batch != tt.want { t.Fatalf("Batch = %q, want %q", req.Batch, tt.want) } }) } } func TestResolveRunRejectsOutputFlag(t *testing.T) { _, err := resolveRun([]string{"morning", "--out", "./report.md"}) if err == nil { t.Fatal("resolveRun() error = nil, want flag error") } if !strings.Contains(err.Error(), "flag provided but not defined") { t.Fatalf("error = %q, want undefined flag error", err.Error()) } } func fixedClock() timeutil.Clock { return timeutil.FixedClock{Time: time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)} }