// outlooks_read_test.go validates outlook repository read flow.
// Layer: adapters/outbound/postgres outlook read tests.
package postgres

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"errors"
	"fmt"
	"io"
	"strings"
	"sync"
	"testing"
	"time"
)

const outlookTestDriverName = "weatherapi_outlook_read_test"

func init() {
	sql.Register(outlookTestDriverName, outlookTestDriver{})
}

func TestLatestConvectiveOutlookRunLoadsOutlooksAndDiscussions(t *testing.T) {
	asOf := time.Date(2026, 6, 11, 18, 0, 0, 0, time.UTC)
	issuedAt := asOf.Add(-1 * time.Hour)
	discussionUpdated := asOf.Add(-30 * time.Minute)
	repo, closeDB := openOutlookTestRepository(t,
		outlookParentQuery([][]driver.Value{{
			"evt-outlook-run",
			"stl",
			"St. Louis",
			float64(38.62),
			float64(-90.2),
			asOf,
			issuedAt,
		}}),
		outlookRowsQuery([][]driver.Value{
			outlookReadRow(1, "day-1", 1, "categorical"),
			outlookReadRow(2, "day-2", 2, "wind"),
		}, nil),
		outlookDiscussionsQuery([][]driver.Value{
			{int64(1), int64(1), "Day 1 headline", "Day 1 summary", "Day 1 discussion", discussionUpdated},
			{int64(2), int64(2), "Day 2 headline", nil, "Day 2 discussion", nil},
		}, nil),
	)
	defer closeDB()

	run, err := repo.LatestConvectiveOutlookRun(context.Background())
	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" {
		t.Fatalf("unexpected run metadata: %+v", run)
	}
	if len(run.Outlooks) != 2 {
		t.Fatalf("expected 2 outlooks, got %d", len(run.Outlooks))
	}
	if run.Outlooks[0].ID != "day-1" || run.Outlooks[1].ID != "day-2" {
		t.Fatalf("expected outlook order from rows, got %+v", run.Outlooks)
	}
	if len(run.Discussions) != 2 {
		t.Fatalf("expected 2 discussions, got %d", len(run.Discussions))
	}
	if run.Discussions[0].Day != 1 || run.Discussions[0].Headline != "Day 1 headline" {
		t.Fatalf("unexpected first discussion: %+v", run.Discussions[0])
	}
	if run.Discussions[1].Day != 2 || run.Discussions[1].Summary != "" {
		t.Fatalf("unexpected second discussion: %+v", run.Discussions[1])
	}
	if run.Discussions[0].UpdatedAt == nil || run.Discussions[0].UpdatedAt.Location().String() != "UTC" {
		t.Fatalf("expected discussion updatedAt UTC pointer, got %v", run.Discussions[0].UpdatedAt)
	}
	if run.Discussions[1].UpdatedAt != nil {
		t.Fatalf("expected nil discussion updatedAt, got %v", run.Discussions[1].UpdatedAt)
	}
	assertOutlookTestQueriesConsumed(t)
}

func TestLatestConvectiveOutlookRunMissingParentReturnsNil(t *testing.T) {
	repo, closeDB := openOutlookTestRepository(t, outlookParentQuery(nil))
	defer closeDB()

	run, err := repo.LatestConvectiveOutlookRun(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if run != nil {
		t.Fatalf("expected nil run, got %+v", run)
	}
	assertOutlookTestQueriesConsumed(t)
}

func TestLatestConvectiveOutlookRunWrapsReadErrors(t *testing.T) {
	asOf := time.Date(2026, 6, 11, 18, 0, 0, 0, time.UTC)

	tests := []struct {
		name    string
		queries []scriptedOutlookQuery
		want    string
	}{
		{
			name: "parent query",
			queries: []scriptedOutlookQuery{
				{name: "parent", query: queryLatestConvectiveOutlookRun, err: errors.New("parent unavailable")},
			},
			want: "query latest convective outlook run",
		},
		{
			name: "outlooks query",
			queries: []scriptedOutlookQuery{
				outlookParentQuery([][]driver.Value{{"evt-outlook-run", nil, nil, nil, nil, asOf, nil}}),
				{name: "outlooks", query: queryOutlooksForRun, args: []driver.Value{"evt-outlook-run"}, err: errors.New("outlooks unavailable")},
			},
			want: "query outlooks",
		},
		{
			name: "outlook scan",
			queries: []scriptedOutlookQuery{
				outlookParentQuery([][]driver.Value{{"evt-outlook-run", nil, nil, nil, nil, asOf, nil}}),
				outlookRowsQuery([][]driver.Value{
					{int64(1), "day-1"},
				}, nil),
			},
			want: "scan outlook row",
		},
		{
			name: "outlook map",
			queries: []scriptedOutlookQuery{
				outlookParentQuery([][]driver.Value{{"evt-outlook-run", nil, nil, nil, nil, asOf, nil}}),
				outlookRowsQuery([][]driver.Value{
					outlookReadRowWithGeometry(1, "day-1", 1, "categorical", `{"type":"Point"`),
				}, nil),
			},
			want: "map outlook row",
		},
		{
			name: "outlook iteration",
			queries: []scriptedOutlookQuery{
				outlookParentQuery([][]driver.Value{{"evt-outlook-run", nil, nil, nil, nil, asOf, nil}}),
				outlookRowsQuery([][]driver.Value{
					outlookReadRow(1, "day-1", 1, "categorical"),
				}, errors.New("outlook iteration failed")),
			},
			want: "iterate outlook rows",
		},
		{
			name: "discussions query",
			queries: []scriptedOutlookQuery{
				outlookParentQuery([][]driver.Value{{"evt-outlook-run", nil, nil, nil, nil, asOf, nil}}),
				outlookRowsQuery(nil, nil),
				{name: "discussions", query: queryOutlookDiscussionsForRun, args: []driver.Value{"evt-outlook-run"}, err: errors.New("discussions unavailable")},
			},
			want: "query outlook discussions",
		},
		{
			name: "discussion scan",
			queries: []scriptedOutlookQuery{
				outlookParentQuery([][]driver.Value{{"evt-outlook-run", nil, nil, nil, nil, asOf, nil}}),
				outlookRowsQuery(nil, nil),
				outlookDiscussionsQuery([][]driver.Value{
					{int64(1), "bad day", nil, nil, nil, nil},
				}, nil),
			},
			want: "scan outlook discussion row",
		},
		{
			name: "discussion iteration",
			queries: []scriptedOutlookQuery{
				outlookParentQuery([][]driver.Value{{"evt-outlook-run", nil, nil, nil, nil, asOf, nil}}),
				outlookRowsQuery(nil, nil),
				outlookDiscussionsQuery([][]driver.Value{
					{int64(1), int64(1), nil, nil, nil, nil},
				}, errors.New("discussion iteration failed")),
			},
			want: "iterate outlook discussion rows",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			repo, closeDB := openOutlookTestRepository(t, tt.queries...)
			defer closeDB()

			_, err := repo.LatestConvectiveOutlookRun(context.Background())
			if err == nil {
				t.Fatal("expected error")
			}
			if !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("expected error containing %q, got %v", tt.want, err)
			}
		})
	}
}

func openOutlookTestRepository(t *testing.T, queries ...scriptedOutlookQuery) (*Repository, func()) {
	t.Helper()
	outlookTestScript.set(queries)

	db, err := sql.Open(outlookTestDriverName, "")
	if err != nil {
		t.Fatalf("open test db: %v", err)
	}
	db.SetMaxOpenConns(1)

	return NewRepository(db), func() {
		_ = db.Close()
		outlookTestScript.set(nil)
	}
}

func assertOutlookTestQueriesConsumed(t *testing.T) {
	t.Helper()
	if remaining := outlookTestScript.remaining(); remaining != 0 {
		t.Fatalf("expected all scripted queries consumed, got %d remaining", remaining)
	}
}

func outlookParentQuery(rows [][]driver.Value) scriptedOutlookQuery {
	return scriptedOutlookQuery{
		name:    "parent",
		query:   queryLatestConvectiveOutlookRun,
		columns: []string{"event_id", "location_id", "location_name", "latitude", "longitude", "as_of", "issued_at"},
		rows:    rows,
	}
}

func outlookRowsQuery(rows [][]driver.Value, nextErr error) scriptedOutlookQuery {
	return scriptedOutlookQuery{
		name:    "outlooks",
		query:   queryOutlooksForRun,
		args:    []driver.Value{"evt-outlook-run"},
		columns: []string{"outlook_index", "outlook_id", "provider", "product", "day", "outlook_type", "label", "label_text", "severity_rank", "valid_from", "valid_to", "issued_at", "expires_at", "forecaster", "source_url", "image_url", "contains_location", "geometry_json"},
		rows:    rows,
		nextErr: nextErr,
	}
}

func outlookDiscussionsQuery(rows [][]driver.Value, nextErr error) scriptedOutlookQuery {
	return scriptedOutlookQuery{
		name:    "discussions",
		query:   queryOutlookDiscussionsForRun,
		args:    []driver.Value{"evt-outlook-run"},
		columns: []string{"discussion_index", "day", "headline", "summary", "discussion", "updated_at"},
		rows:    rows,
		nextErr: nextErr,
	}
}

func outlookReadRow(index int64, id string, day int64, outlookType string) []driver.Value {
	return outlookReadRowWithGeometry(index, id, day, outlookType, `{"type":"Polygon","coordinates":[[[-91,38],[-90,38],[-90,39],[-91,38]]]}`)
}

func outlookReadRowWithGeometry(index int64, id string, day int64, outlookType string, geometry string) []driver.Value {
	validFrom := time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC)
	validTo := validFrom.Add(6 * time.Hour)
	return []driver.Value{
		index,
		id,
		"spc",
		"convective",
		day,
		outlookType,
		"SLGT",
		"Slight Risk",
		int64(5),
		validFrom,
		validTo,
		validFrom.Add(-1 * time.Hour),
		validTo,
		"DIAL",
		"https://example.test/source",
		"https://example.test/image.png",
		true,
		geometry,
	}
}

type outlookTestDriver struct{}

func (outlookTestDriver) Open(string) (driver.Conn, error) {
	return outlookTestConn{}, nil
}

type outlookTestConn struct{}

func (outlookTestConn) Prepare(string) (driver.Stmt, error) {
	return nil, errors.New("prepare is not supported")
}

func (outlookTestConn) Close() error {
	return nil
}

func (outlookTestConn) Begin() (driver.Tx, error) {
	return nil, errors.New("transactions are not supported")
}

func (outlookTestConn) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
	return outlookTestScript.next(query, args)
}

type scriptedOutlookQuery struct {
	name    string
	query   string
	args    []driver.Value
	columns []string
	rows    [][]driver.Value
	err     error
	nextErr error
}

type outlookTestScriptState struct {
	mu      sync.Mutex
	queries []scriptedOutlookQuery
}

var outlookTestScript outlookTestScriptState

func (s *outlookTestScriptState) set(queries []scriptedOutlookQuery) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.queries = append([]scriptedOutlookQuery(nil), queries...)
}

func (s *outlookTestScriptState) remaining() int {
	s.mu.Lock()
	defer s.mu.Unlock()
	return len(s.queries)
}

func (s *outlookTestScriptState) next(query string, args []driver.NamedValue) (driver.Rows, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	if len(s.queries) == 0 {
		return nil, fmt.Errorf("unexpected query: %s", compactSQL(query))
	}
	next := s.queries[0]
	s.queries = s.queries[1:]

	if compactSQL(query) != compactSQL(next.query) {
		return nil, fmt.Errorf("expected %s query %q, got %q", next.name, compactSQL(next.query), compactSQL(query))
	}
	if len(args) != len(next.args) {
		return nil, fmt.Errorf("expected %s args %v, got %v", next.name, next.args, namedValues(args))
	}
	for i, arg := range args {
		if arg.Value != next.args[i] {
			return nil, fmt.Errorf("expected %s arg %d to be %v, got %v", next.name, i, next.args[i], arg.Value)
		}
	}
	if next.err != nil {
		return nil, next.err
	}
	return &outlookTestRows{
		columns: append([]string(nil), next.columns...),
		rows:    append([][]driver.Value(nil), next.rows...),
		nextErr: next.nextErr,
	}, nil
}

type outlookTestRows struct {
	columns []string
	rows    [][]driver.Value
	index   int
	nextErr error
}

func (r *outlookTestRows) Columns() []string {
	return r.columns
}

func (r *outlookTestRows) Close() error {
	return nil
}

func (r *outlookTestRows) Next(dest []driver.Value) error {
	if r.index >= len(r.rows) {
		if r.nextErr != nil {
			err := r.nextErr
			r.nextErr = nil
			return err
		}
		return io.EOF
	}
	copy(dest, r.rows[r.index])
	r.index++
	return nil
}

func compactSQL(query string) string {
	return strings.Join(strings.Fields(query), " ")
}

func namedValues(args []driver.NamedValue) []driver.Value {
	out := make([]driver.Value, len(args))
	for i := range args {
		out[i] = args[i].Value
	}
	return out
}
