// outlooks_mapper_test.go validates outlook row mapping.
// Layer: adapters/outbound/postgres outlook mapper tests.
package postgres

import (
	"database/sql"
	"testing"
	"time"
)

func TestMapOutlookRunParentRowNullables(t *testing.T) {
	asOf := time.Date(2026, 6, 11, 11, 30, 0, 0, time.FixedZone("CDT", -5*3600))
	issuedAt := asOf.Add(-30 * time.Minute)
	latitude := 38.62

	run := mapOutlookRunParentRow(outlookRunParentRow{
		EventID:      "evt-outlook-run",
		LocationID:   sql.NullString{String: "stl", Valid: true},
		LocationName: sql.NullString{String: "St. Louis", Valid: true},
		Latitude:     sql.NullFloat64{Float64: latitude, Valid: true},
		Longitude:    sql.NullFloat64{Valid: false},
		AsOf:         asOf,
		IssuedAt:     sql.NullTime{Time: issuedAt, Valid: true},
	})

	if run.LocationID != "stl" || run.LocationName != "St. Louis" {
		t.Fatalf("unexpected location metadata: %+v", run)
	}
	if run.Latitude == nil || *run.Latitude != latitude {
		t.Fatalf("expected latitude pointer %v, got %v", latitude, run.Latitude)
	}
	if run.Longitude != nil {
		t.Fatalf("expected nil longitude, got %v", *run.Longitude)
	}
	if run.AsOf.Location().String() != "UTC" {
		t.Fatalf("expected asOf UTC normalization, got %s", run.AsOf.Location())
	}
	if run.IssuedAt == nil || run.IssuedAt.Location().String() != "UTC" {
		t.Fatalf("expected issuedAt UTC pointer, got %v", run.IssuedAt)
	}
	if run.Outlooks != nil {
		t.Fatalf("expected nil outlooks before child load, got %+v", run.Outlooks)
	}
}

func TestMapOutlookRunParentRowMissingOptionals(t *testing.T) {
	run := mapOutlookRunParentRow(outlookRunParentRow{
		AsOf: time.Date(2026, 6, 11, 16, 30, 0, 0, time.UTC),
	})

	if run.LocationID != "" || run.LocationName != "" {
		t.Fatalf("expected empty location metadata, got %+v", run)
	}
	if run.Latitude != nil || run.Longitude != nil || run.IssuedAt != nil {
		t.Fatalf("expected nil optional pointers, got %+v", run)
	}
}

func TestMapOutlookRowMapsFields(t *testing.T) {
	validFrom := time.Date(2026, 6, 11, 7, 0, 0, 0, time.FixedZone("CDT", -5*3600))
	validTo := validFrom.Add(12 * time.Hour)
	issuedAt := validFrom.Add(-1 * time.Hour)
	expiresAt := validTo
	severityRank := int64(5)
	geometry := `{"type":"Polygon","coordinates":[[[-91,38],[-90,38],[-90,39],[-91,38]]]}`

	outlook, err := mapOutlookRow(outlookRow{
		OutlookIndex:     3,
		OutlookID:        "spc-20260611-1300-day1-cat-slight",
		Provider:         "spc",
		Product:          "convective",
		Day:              1,
		OutlookType:      "categorical",
		Label:            "SLGT",
		LabelText:        sql.NullString{String: "Slight Risk", Valid: true},
		SeverityRank:     sql.NullInt64{Int64: severityRank, Valid: true},
		ValidFrom:        validFrom,
		ValidTo:          validTo,
		IssuedAt:         issuedAt,
		ExpiresAt:        expiresAt,
		Forecaster:       sql.NullString{String: "DIAL", Valid: true},
		SourceURL:        sql.NullString{String: "https://www.spc.noaa.gov/products/outlook/day1otlk.html", Valid: true},
		ImageURL:         sql.NullString{String: "https://www.spc.noaa.gov/products/outlook/day1otlk.gif", Valid: true},
		ContainsLocation: true,
		GeometryJSON:     geometry,
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if outlook.ID != "spc-20260611-1300-day1-cat-slight" {
		t.Fatalf("unexpected outlook id: %q", outlook.ID)
	}
	if outlook.Provider != "spc" || outlook.Product != "convective" || outlook.Day != 1 || outlook.OutlookType != "categorical" {
		t.Fatalf("unexpected core fields: %+v", outlook)
	}
	if outlook.Label != "SLGT" || outlook.LabelText != "Slight Risk" {
		t.Fatalf("unexpected label fields: %+v", outlook)
	}
	if outlook.SeverityRank == nil || *outlook.SeverityRank != int(severityRank) {
		t.Fatalf("expected severity rank %d, got %v", severityRank, outlook.SeverityRank)
	}
	if outlook.Forecaster != "DIAL" {
		t.Fatalf("unexpected forecaster: %+v", outlook)
	}
	if outlook.SourceURL == "" || outlook.ImageURL == "" {
		t.Fatalf("expected source and image URLs: %+v", outlook)
	}
	if !outlook.ContainsLocation {
		t.Fatal("expected containsLocation true")
	}
	if outlook.ValidFrom.Location().String() != "UTC" ||
		outlook.ValidTo.Location().String() != "UTC" ||
		outlook.IssuedAt.Location().String() != "UTC" ||
		outlook.ExpiresAt.Location().String() != "UTC" {
		t.Fatalf("expected UTC timestamps, got %s %s %s %s", outlook.ValidFrom, outlook.ValidTo, outlook.IssuedAt, outlook.ExpiresAt)
	}
	if string(outlook.Geometry) != geometry {
		t.Fatalf("expected geometry bytes preserved, got %s", outlook.Geometry)
	}
}

func TestMapOutlookRowMissingOptionals(t *testing.T) {
	outlook, err := mapOutlookRow(outlookRow{
		OutlookID:        "tor-1",
		Provider:         "spc",
		Product:          "convective",
		Day:              1,
		OutlookType:      "tornado",
		Label:            "2%",
		ValidFrom:        time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC),
		ValidTo:          time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC),
		IssuedAt:         time.Date(2026, 6, 11, 11, 0, 0, 0, time.UTC),
		ExpiresAt:        time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC),
		ContainsLocation: false,
		GeometryJSON:     `{"type":"Point","coordinates":[-90.2,38.6]}`,
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if outlook.LabelText != "" || outlook.Forecaster != "" || outlook.SourceURL != "" || outlook.ImageURL != "" {
		t.Fatalf("expected optional strings to map to empty values, got %+v", outlook)
	}
	if outlook.SeverityRank != nil {
		t.Fatalf("expected nil severity rank, got %v", *outlook.SeverityRank)
	}
	if outlook.ContainsLocation {
		t.Fatal("expected containsLocation false")
	}
}

func TestMapOutlookRowRejectsInvalidGeometry(t *testing.T) {
	_, err := mapOutlookRow(outlookRow{
		OutlookID:    "bad-geometry",
		Provider:     "spc",
		Product:      "convective",
		Day:          1,
		OutlookType:  "categorical",
		Label:        "SLGT",
		ValidFrom:    time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC),
		ValidTo:      time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC),
		IssuedAt:     time.Date(2026, 6, 11, 11, 0, 0, 0, time.UTC),
		ExpiresAt:    time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC),
		GeometryJSON: `{"type":"Point"`,
	})
	if err == nil {
		t.Fatal("expected invalid geometry error")
	}
}

func TestMapOutlookDiscussionRowMapsFields(t *testing.T) {
	updatedAt := time.Date(2026, 6, 11, 7, 30, 0, 0, time.FixedZone("CDT", -5*3600))

	discussion := mapOutlookDiscussionRow(outlookDiscussionRow{
		DiscussionIndex: 2,
		Day:             2,
		Headline:        sql.NullString{String: "Severe storms possible", Valid: true},
		Summary:         sql.NullString{String: "Scattered severe storms are possible.", Valid: true},
		Discussion:      sql.NullString{String: "Discussion text.", Valid: true},
		UpdatedAt:       sql.NullTime{Time: updatedAt, Valid: true},
	})

	if discussion.Day != 2 {
		t.Fatalf("expected day 2, got %d", discussion.Day)
	}
	if discussion.Headline != "Severe storms possible" {
		t.Fatalf("unexpected headline: %q", discussion.Headline)
	}
	if discussion.Summary != "Scattered severe storms are possible." {
		t.Fatalf("unexpected summary: %q", discussion.Summary)
	}
	if discussion.Discussion != "Discussion text." {
		t.Fatalf("unexpected discussion: %q", discussion.Discussion)
	}
	if discussion.UpdatedAt == nil || discussion.UpdatedAt.Location().String() != "UTC" {
		t.Fatalf("expected updatedAt UTC pointer, got %v", discussion.UpdatedAt)
	}
}

func TestMapOutlookDiscussionRowMissingOptionals(t *testing.T) {
	discussion := mapOutlookDiscussionRow(outlookDiscussionRow{
		DiscussionIndex: 1,
		Day:             1,
	})

	if discussion.Day != 1 {
		t.Fatalf("expected day 1, got %d", discussion.Day)
	}
	if discussion.Headline != "" || discussion.Summary != "" || discussion.Discussion != "" {
		t.Fatalf("expected empty optional strings, got %+v", discussion)
	}
	if discussion.UpdatedAt != nil {
		t.Fatalf("expected nil updatedAt, got %v", discussion.UpdatedAt)
	}
}
