// service_test.go validates application service delegation behavior.
// Layer: internal/app tests for read use-case orchestration.
package app

import (
	"context"
	"errors"
	"testing"
	"time"

	"gitea.maximumdirect.net/ejr/weatherfeeder/model"
)

type fakeRepository struct {
	observation *model.WeatherObservation
	forecast    *model.WeatherForecastRun
	narrative   *model.WeatherForecastRun
	discussion  *model.WeatherForecastDiscussion
	storyRun    *model.WeatherStoryRun
	story       *model.WeatherStory
	alerts      *model.WeatherAlertRun
	outlookRun  *model.WeatherOutlookRun
	conditions  *CurrentConditions
	err         error

	currentConditionsWindow int
	outlookRunCalls         int
}

func (r *fakeRepository) LatestObservation(context.Context) (*model.WeatherObservation, error) {
	return r.observation, r.err
}

func (r *fakeRepository) LatestHourlyForecast(context.Context) (*model.WeatherForecastRun, error) {
	return r.forecast, r.err
}

func (r *fakeRepository) LatestNarrativeForecast(context.Context) (*model.WeatherForecastRun, error) {
	return r.narrative, r.err
}

func (r *fakeRepository) LatestForecastDiscussion(context.Context) (*model.WeatherForecastDiscussion, error) {
	return r.discussion, r.err
}

func (r *fakeRepository) LatestWeatherStoryRun(context.Context) (*model.WeatherStoryRun, error) {
	return r.storyRun, r.err
}

func (r *fakeRepository) LatestWeatherStory(context.Context) (*model.WeatherStory, error) {
	return r.story, r.err
}

func (r *fakeRepository) LatestAlertRun(context.Context) (*model.WeatherAlertRun, error) {
	return r.alerts, r.err
}

func (r *fakeRepository) LatestConvectiveOutlookRun(context.Context) (*model.WeatherOutlookRun, error) {
	r.outlookRunCalls++
	return r.outlookRun, r.err
}

func (r *fakeRepository) CurrentConditions(_ context.Context, observationWindowMinutes int) (*CurrentConditions, error) {
	r.currentConditionsWindow = observationWindowMinutes
	return r.conditions, r.err
}

func TestServiceDelegatesObservation(t *testing.T) {
	repo := &fakeRepository{observation: &model.WeatherObservation{StationID: "KSTL"}}
	svc := NewService(repo)

	obs, err := svc.LatestObservation(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if obs == nil || obs.StationID != "KSTL" {
		t.Fatalf("unexpected observation: %+v", obs)
	}
}

func TestServiceDelegatesForecast(t *testing.T) {
	repo := &fakeRepository{forecast: &model.WeatherForecastRun{LocationID: "stl"}}
	svc := NewService(repo)

	run, err := svc.LatestHourlyForecast(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if run == nil || run.LocationID != "stl" {
		t.Fatalf("unexpected forecast: %+v", run)
	}
}

func TestServiceDelegatesNarrativeForecast(t *testing.T) {
	repo := &fakeRepository{narrative: &model.WeatherForecastRun{LocationID: "stl-narrative"}}
	svc := NewService(repo)

	run, err := svc.LatestNarrativeForecast(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if run == nil || run.LocationID != "stl-narrative" {
		t.Fatalf("unexpected forecast: %+v", run)
	}
}

func TestServiceDelegatesAlerts(t *testing.T) {
	repo := &fakeRepository{alerts: &model.WeatherAlertRun{LocationID: "stl"}}
	svc := NewService(repo)

	run, err := svc.LatestAlertRun(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if run == nil || run.LocationID != "stl" {
		t.Fatalf("unexpected alert run: %+v", run)
	}
}

func TestServiceDelegatesLatestConvectiveOutlookRun(t *testing.T) {
	repo := &fakeRepository{outlookRun: testOutlookRun()}
	svc := NewService(repo)

	run, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if repo.outlookRunCalls != 1 {
		t.Fatalf("expected one repository call, got %d", repo.outlookRunCalls)
	}
	if run == nil || run.LocationID != "stl" {
		t.Fatalf("unexpected outlook run: %+v", run)
	}
}

func TestServiceLatestConvectiveOutlookNoData(t *testing.T) {
	repo := &fakeRepository{}
	svc := NewService(repo)

	run, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if run != nil {
		t.Fatalf("expected nil outlook run, got %+v", run)
	}
}

func TestServiceDelegatesForecastDiscussion(t *testing.T) {
	repo := &fakeRepository{discussion: &model.WeatherForecastDiscussion{OfficeID: "LSX"}}
	svc := NewService(repo)

	run, err := svc.LatestForecastDiscussion(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if run == nil || run.OfficeID != "LSX" {
		t.Fatalf("unexpected forecast discussion: %+v", run)
	}
}

func TestServiceDelegatesWeatherStoryRun(t *testing.T) {
	repo := &fakeRepository{storyRun: &model.WeatherStoryRun{OfficeID: "LSX"}}
	svc := NewService(repo)

	run, err := svc.LatestWeatherStoryRun(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if run == nil || run.OfficeID != "LSX" {
		t.Fatalf("unexpected weather story run: %+v", run)
	}
}

func TestServiceDelegatesWeatherStory(t *testing.T) {
	repo := &fakeRepository{story: &model.WeatherStory{OfficeID: "LSX", Title: "Rain chances"}}
	svc := NewService(repo)

	story, err := svc.LatestWeatherStory(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if story == nil || story.Title != "Rain chances" {
		t.Fatalf("unexpected weather story: %+v", story)
	}
}

func TestServiceUsesDefaultCurrentConditionsWindow(t *testing.T) {
	repo := &fakeRepository{conditions: &CurrentConditions{ConditionCode: model.WMOUnknown}}
	svc := NewService(repo)

	_, err := svc.CurrentConditions(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if repo.currentConditionsWindow != ObservationWindowMinutesDefault {
		t.Fatalf("expected observation window %d, got %d", ObservationWindowMinutesDefault, repo.currentConditionsWindow)
	}
}

func TestServiceLatestConvectiveOutlookFiltersByDay(t *testing.T) {
	day := 2
	repo := &fakeRepository{outlookRun: testOutlookRun()}
	svc := NewService(repo)

	run, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{Day: &day})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	assertOutlookIDs(t, run, []string{"day-2"})
}

func TestServiceLatestConvectiveOutlookFiltersByOutlookType(t *testing.T) {
	repo := &fakeRepository{outlookRun: testOutlookRun()}
	svc := NewService(repo)

	run, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{OutlookType: " Tornado "})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	assertOutlookIDs(t, run, []string{"tor-1"})
}

func TestServiceLatestConvectiveOutlookFiltersByContainsLocation(t *testing.T) {
	containsLocation := true
	repo := &fakeRepository{outlookRun: testOutlookRun()}
	svc := NewService(repo)

	run, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{ContainsLocation: &containsLocation})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	assertOutlookIDs(t, run, []string{"cat-1", "tor-1"})
}

func TestServiceLatestConvectiveOutlookFiltersByActiveAt(t *testing.T) {
	activeAt := time.Date(2026, 6, 11, 15, 0, 0, 0, time.UTC)
	repo := &fakeRepository{outlookRun: testOutlookRun()}
	svc := NewService(repo)

	run, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{ActiveAt: &activeAt})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	assertOutlookIDs(t, run, []string{"cat-1", "tor-1"})
}

func TestServiceLatestConvectiveOutlookCombinesFilters(t *testing.T) {
	day := 1
	containsLocation := true
	activeAt := time.Date(2026, 6, 11, 15, 0, 0, 0, time.UTC)
	repo := &fakeRepository{outlookRun: testOutlookRun()}
	svc := NewService(repo)

	run, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{
		Day:              &day,
		OutlookType:      "categorical",
		ContainsLocation: &containsLocation,
		ActiveAt:         &activeAt,
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	assertOutlookIDs(t, run, []string{"cat-1"})
}

func TestServiceLatestConvectiveOutlookActiveAtBoundary(t *testing.T) {
	run := testOutlookRun()
	run.Outlooks = run.Outlooks[:1]
	validFrom := run.Outlooks[0].ValidFrom
	validTo := run.Outlooks[0].ValidTo
	repo := &fakeRepository{outlookRun: run}
	svc := NewService(repo)

	fromRun, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{ActiveAt: &validFrom})
	if err != nil {
		t.Fatalf("unexpected validFrom error: %v", err)
	}
	assertOutlookIDs(t, fromRun, []string{"cat-1"})

	toRun, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{ActiveAt: &validTo})
	if err != nil {
		t.Fatalf("unexpected validTo error: %v", err)
	}
	assertOutlookIDs(t, toRun, nil)
}

func TestServiceLatestConvectiveOutlookKeepsRunMetadataWithEmptyOutlooks(t *testing.T) {
	day := 3
	repo := &fakeRepository{outlookRun: testOutlookRun()}
	svc := NewService(repo)

	run, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{Day: &day})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if run == nil {
		t.Fatal("expected outlook run")
	}
	if run.LocationID != "stl" || run.LocationName != "St. Louis" || !run.AsOf.Equal(testTime(12)) {
		t.Fatalf("unexpected run metadata: %+v", run)
	}
	if run.Outlooks == nil {
		t.Fatal("expected empty outlook slice, got nil")
	}
	if len(run.Outlooks) != 0 {
		t.Fatalf("expected no outlooks, got %+v", run.Outlooks)
	}
}

func TestServiceLatestConvectiveOutlookDoesNotMutateRepositoryRun(t *testing.T) {
	original := testOutlookRun()
	repo := &fakeRepository{outlookRun: original}
	svc := NewService(repo)

	run, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(run.Outlooks) == 0 {
		t.Fatal("expected outlooks")
	}

	*run.Latitude = 99
	*run.Longitude = -99
	*run.IssuedAt = testTime(99)
	*run.Outlooks[0].SeverityRank = 99
	run.Outlooks[0].Geometry[0] = '{'
	run.Outlooks[0].ID = "changed"
	run.Outlooks = run.Outlooks[:1]

	if *original.Latitude != 38.62 {
		t.Fatalf("expected original latitude unchanged, got %v", *original.Latitude)
	}
	if *original.Longitude != -90.2 {
		t.Fatalf("expected original longitude unchanged, got %v", *original.Longitude)
	}
	if !original.IssuedAt.Equal(testTime(11)) {
		t.Fatalf("expected original issuedAt unchanged, got %v", original.IssuedAt)
	}
	if *original.Outlooks[0].SeverityRank != 5 {
		t.Fatalf("expected original severity rank unchanged, got %v", *original.Outlooks[0].SeverityRank)
	}
	if string(original.Outlooks[0].Geometry) != `["cat"]` {
		t.Fatalf("expected original geometry unchanged, got %s", original.Outlooks[0].Geometry)
	}
	if original.Outlooks[0].ID != "cat-1" {
		t.Fatalf("expected original outlook ID unchanged, got %q", original.Outlooks[0].ID)
	}
	if len(original.Outlooks) != 3 {
		t.Fatalf("expected original outlook slice unchanged, got %d entries", len(original.Outlooks))
	}
}

func TestServicePropagatesErrors(t *testing.T) {
	want := errors.New("boom")
	repo := &fakeRepository{err: want}
	svc := NewService(repo)

	if _, err := svc.LatestObservation(context.Background()); !errors.Is(err, want) {
		t.Fatalf("expected error %v, got %v", want, err)
	}
}

func testOutlookRun() *model.WeatherOutlookRun {
	latitude := 38.62
	longitude := -90.2
	issuedAt := testTime(11)
	return &model.WeatherOutlookRun{
		LocationID:   "stl",
		LocationName: "St. Louis",
		Latitude:     &latitude,
		Longitude:    &longitude,
		AsOf:         testTime(12),
		IssuedAt:     &issuedAt,
		Outlooks: []model.WeatherOutlook{
			testOutlook("cat-1", 1, "categorical", true, testTime(12), testTime(18), 5, `["cat"]`),
			testOutlook("tor-1", 1, "tornado", true, testTime(13), testTime(19), 7, `["tor"]`),
			testOutlook("day-2", 2, "wind", false, testTime(18), testTime(24), 2, `["wind"]`),
		},
	}
}

func testOutlook(id string, day int, outlookType string, containsLocation bool, validFrom time.Time, validTo time.Time, severityRank int, geometry string) model.WeatherOutlook {
	return model.WeatherOutlook{
		ID:               id,
		Provider:         "spc",
		Product:          "convective",
		Day:              day,
		OutlookType:      outlookType,
		Label:            "SLGT",
		LabelText:        "Slight Risk",
		SeverityRank:     &severityRank,
		ValidFrom:        validFrom,
		ValidTo:          validTo,
		IssuedAt:         validFrom.Add(-time.Hour),
		ExpiresAt:        validTo,
		Forecaster:       "DIAL",
		SourceURL:        "https://example.test/" + id,
		ImageURL:         "https://example.test/" + id + ".png",
		ContainsLocation: containsLocation,
		Geometry:         []byte(geometry),
	}
}

func testTime(hour int) time.Time {
	return time.Date(2026, 6, 11, hour, 0, 0, 0, time.UTC)
}

func assertOutlookIDs(t *testing.T, run *model.WeatherOutlookRun, want []string) {
	t.Helper()
	if run == nil {
		t.Fatal("expected outlook run")
	}
	if len(run.Outlooks) != len(want) {
		t.Fatalf("expected outlook IDs %v, got %+v", want, run.Outlooks)
	}
	for i := range want {
		if run.Outlooks[i].ID != want[i] {
			t.Fatalf("expected outlook IDs %v, got %+v", want, run.Outlooks)
		}
	}
}
