package web

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/AmanTahiliani/box-box/internal/api"
	"github.com/AmanTahiliani/box-box/internal/models"
	"github.com/AmanTahiliani/box-box/internal/store"
)

func driverSummaryFixtures() ([]meetingRace, []models.ChampionshipDriver, map[int]models.Driver) {
	driverInfo := map[int]models.Driver{
		1: {DriverNumber: 1, NameAcronym: "VER", FullName: "Max Verstappen", TeamName: "Red Bull", TeamColour: "3671c6", HeadshotURL: "https://example.com/ver.png"},
		3: {DriverNumber: 3, NameAcronym: "HAM", FullName: "Lewis Hamilton", TeamName: "Mercedes", TeamColour: "27f4d2"},
	}

	champ := []models.ChampionshipDriver{
		{DriverNumber: 1, PointsCurrent: 50, PositionCurrent: 1, SessionKey: 99},
		{DriverNumber: 3, PointsCurrent: 33, PositionCurrent: 2, SessionKey: 99},
	}

	// Round 1: VER P1 from pole, HAM P2 from P3.
	// Round 2: VER P1 from P2, HAM P3 from pole.
	// Round 3: not yet run — must not appear in rounds.
	races := []meetingRace{
		{
			Meeting: models.Meeting{MeetingKey: 1201, MeetingName: "Bahrain GP", CountryCode: "BHR", CountryName: "Bahrain"},
			Results: []models.SessionResult{raceResult(1, 1, 25), raceResult(3, 2, 18)},
			Grid:    []models.StartingGrid{{DriverNumber: 1, Position: 1}, {DriverNumber: 3, Position: 3}},
		},
		{
			Meeting: models.Meeting{MeetingKey: 1202, MeetingName: "Saudi GP", CountryCode: "SAU", CountryName: "Saudi Arabia"},
			Results: []models.SessionResult{raceResult(1, 1, 25), raceResult(3, 3, 15)},
			Grid:    []models.StartingGrid{{DriverNumber: 3, Position: 1}, {DriverNumber: 1, Position: 2}},
		},
		{Meeting: models.Meeting{MeetingKey: 1203, MeetingName: "Australia GP"}},
	}
	return races, champ, driverInfo
}

func TestAggregateDriverSummary(t *testing.T) {
	races, champ, driverInfo := driverSummaryFixtures()

	resp, ok := aggregateDriverSummary(2025, 1, races, champ, driverInfo)
	if !ok {
		t.Fatal("aggregateDriverSummary ok = false, want true")
	}

	if resp.Season != 2025 || resp.DriverNumber != 1 {
		t.Errorf("season/driver = %d/%d, want 2025/1", resp.Season, resp.DriverNumber)
	}
	if resp.NameAcronym != "VER" || resp.FullName != "Max Verstappen" || resp.TeamName != "Red Bull" || resp.TeamColour != "3671c6" {
		t.Errorf("driver identity = %s %s %s %s", resp.NameAcronym, resp.FullName, resp.TeamName, resp.TeamColour)
	}
	if resp.HeadshotURL != "https://example.com/ver.png" {
		t.Errorf("headshot = %q", resp.HeadshotURL)
	}
	if resp.Points != 50 || resp.Position != 1 {
		t.Errorf("points/position = %v/%d, want 50/1", resp.Points, resp.Position)
	}
	if resp.Wins != 2 || resp.Podiums != 2 || resp.Poles != 1 {
		t.Errorf("wins/podiums/poles = %d/%d/%d, want 2/2/1", resp.Wins, resp.Podiums, resp.Poles)
	}
	if len(resp.Form) != 2 || resp.Form[0] != 25 || resp.Form[1] != 25 {
		t.Errorf("form = %v, want [25 25]", resp.Form)
	}
	// Cumulative reconciles the last value to the official total.
	if len(resp.Cumulative) != 2 || resp.Cumulative[0] != 25 || resp.Cumulative[1] != 50 {
		t.Errorf("cumulative = %v, want [25 50]", resp.Cumulative)
	}
	if len(resp.RoundLabels) != 2 || resp.RoundLabels[0] != "R1" {
		t.Errorf("round labels = %v, want [R1 R2]", resp.RoundLabels)
	}

	// Only completed rounds appear.
	if len(resp.Rounds) != 2 {
		t.Fatalf("rounds = %d, want 2", len(resp.Rounds))
	}
	r1 := resp.Rounds[0]
	if r1.MeetingKey != 1201 || r1.MeetingName != "Bahrain GP" || r1.CountryCode != "BHR" || r1.CountryName != "Bahrain" {
		t.Errorf("round 1 meeting = %+v", r1)
	}
	if r1.RacePosition != 1 || r1.GridPosition != 1 || r1.QualiPosition != 1 || r1.Points != 25 {
		t.Errorf("round 1 result = %+v, want P1 from P1 with 25 pts", r1)
	}
	r2 := resp.Rounds[1]
	if r2.RacePosition != 1 || r2.GridPosition != 2 || r2.Points != 25 {
		t.Errorf("round 2 result = %+v, want P1 from P2 with 25 pts", r2)
	}
}

func TestAggregateDriverSummaryDriverMissingFromRound(t *testing.T) {
	races, champ, driverInfo := driverSummaryFixtures()
	// Drop HAM from round 2's results and grid: the round still appears with
	// zero-valued positions so round indexing stays aligned with the season.
	races[1].Results = []models.SessionResult{raceResult(1, 1, 25)}
	races[1].Grid = []models.StartingGrid{{DriverNumber: 1, Position: 2}}

	resp, ok := aggregateDriverSummary(2025, 3, races, champ, driverInfo)
	if !ok {
		t.Fatal("aggregateDriverSummary ok = false, want true")
	}
	if len(resp.Rounds) != 2 {
		t.Fatalf("rounds = %d, want 2", len(resp.Rounds))
	}
	r2 := resp.Rounds[1]
	if r2.RacePosition != 0 || r2.GridPosition != 0 || r2.QualiPosition != 0 || r2.Points != 0 {
		t.Errorf("round 2 for absent driver = %+v, want zero values", r2)
	}
}

func TestAggregateDriverSummaryUnknownDriver(t *testing.T) {
	races, champ, driverInfo := driverSummaryFixtures()
	if _, ok := aggregateDriverSummary(2025, 44, races, champ, driverInfo); ok {
		t.Error("unknown driver should return ok = false")
	}
}

func TestHandleDriverSummaryBadRequest(t *testing.T) {
	srv := testServer(t, nil)

	// Missing, non-numeric, zero, and negative driver_number are all rejected.
	for _, url := range []string{
		"/api/v1/driver/summary?year=2025",
		"/api/v1/driver/summary?year=2025&driver_number=abc",
		"/api/v1/driver/summary?year=2025&driver_number=0",
		"/api/v1/driver/summary?year=2025&driver_number=-4",
	} {
		req := httptest.NewRequest(http.MethodGet, url, nil)
		rec := httptest.NewRecorder()
		srv.handleDriverSummary(rec, req)
		if rec.Code != http.StatusBadRequest {
			t.Errorf("%s: status = %d, want 400", url, rec.Code)
		}
	}
}

func seedDriverSummaryStore(t *testing.T, st *store.Store) {
	t.Helper()
	meetingKey := 1201
	sessionKey := 9901

	if err := st.UpsertMeeting(store.Meeting{
		MeetingKey:  meetingKey,
		MeetingName: "Bahrain GP",
		CountryCode: "BHR",
		CountryName: "Bahrain",
		Year:        2025,
		DateStart:   "2025-03-02",
	}); err != nil {
		t.Fatalf("UpsertMeeting: %v", err)
	}
	if err := st.UpsertSession(store.Session{
		SessionKey:  sessionKey,
		MeetingKey:  meetingKey,
		SessionName: "Race",
		SessionType: "Race",
		DateStart:   "2025-03-02T15:00:00Z",
	}); err != nil {
		t.Fatalf("UpsertSession: %v", err)
	}
	if err := st.UpsertDriver(store.Driver{
		DriverNumber: 1,
		FullName:     "Max Verstappen",
		NameAcronym:  "VER",
		TeamName:     "Red Bull",
		TeamColour:   "3671c6",
	}); err != nil {
		t.Fatalf("UpsertDriver: %v", err)
	}
	if err := st.UpsertSessionDriver(store.SessionDriver{
		SessionKey:   sessionKey,
		DriverNumber: 1,
		MeetingKey:   meetingKey,
		FullName:     "Max Verstappen",
		NameAcronym:  "VER",
		TeamName:     "Red Bull",
		TeamColour:   "3671c6",
	}); err != nil {
		t.Fatalf("UpsertSessionDriver: %v", err)
	}
	if err := st.UpsertSessionResult(store.SessionResult{
		SessionKey:   sessionKey,
		DriverNumber: 1,
		MeetingKey:   meetingKey,
		Position:     1,
		Points:       25,
	}); err != nil {
		t.Fatalf("UpsertSessionResult: %v", err)
	}
	if err := st.UpsertStartingGridEntry(store.StartingGridEntry{
		SessionKey:   sessionKey,
		DriverNumber: 1,
		MeetingKey:   meetingKey,
		Position:     1,
	}); err != nil {
		t.Fatalf("UpsertStartingGridEntry: %v", err)
	}
}

func TestHandleDriverSummaryLocalFirstIgnoresHangingEnrichment(t *testing.T) {
	prev := driverEnrichmentTimeout
	driverEnrichmentTimeout = 40 * time.Millisecond
	t.Cleanup(func() { driverEnrichmentTimeout = prev })

	st := openTestStore(t)
	seedDriverSummaryStore(t, st)

	// Enrichment seam: OpenF1 hangs until released. Local summary must still return.
	release := make(chan struct{})
	cancelObserved := make(chan struct{})
	hang := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		select {
		case <-release:
		case <-r.Context().Done():
			close(cancelObserved)
		}
	}))
	t.Cleanup(func() {
		close(release)
		hang.Close()
	})

	client := api.NewOpenF1Client(hang.URL, 15*time.Second)
	t.Cleanup(func() { _ = client.Close() })
	srv := NewServer(client, 8080, st)

	start := time.Now()
	req := httptest.NewRequest(http.MethodGet, "/api/v1/driver/summary?year=2025&driver_number=1", nil)
	rec := httptest.NewRecorder()
	srv.handleDriverSummary(rec, req)
	elapsed := time.Since(start)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
	}
	if elapsed > 500*time.Millisecond {
		t.Fatalf("handler blocked on enrichment for %v", elapsed)
	}
	select {
	case <-cancelObserved:
	case <-time.After(250 * time.Millisecond):
		t.Fatal("timed-out enrichment did not cancel its upstream request")
	}

	var resp driverSummaryResponse
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Source != "local" {
		t.Errorf("source = %q, want local", resp.Source)
	}
	if resp.Enrichment != "limited" {
		t.Errorf("enrichment = %q, want limited", resp.Enrichment)
	}
	if rec.Header().Get(dataSourceHeader) != "local" || rec.Header().Get(dataFreshnessHeader) != "limited" {
		t.Errorf("limited metadata = %q/%q", rec.Header().Get(dataSourceHeader), rec.Header().Get(dataFreshnessHeader))
	}
	if resp.DriverNumber != 1 || resp.NameAcronym != "VER" || resp.Points != 25 {
		t.Errorf("local identity/results missing: %+v", resp)
	}
}

func TestHandleDriverSummaryLocalFirstWithFailingEnrichment(t *testing.T) {
	prev := driverEnrichmentTimeout
	driverEnrichmentTimeout = 200 * time.Millisecond
	t.Cleanup(func() { driverEnrichmentTimeout = prev })

	st := openTestStore(t)
	seedDriverSummaryStore(t, st)

	fail := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "boom", http.StatusBadGateway)
	}))
	t.Cleanup(fail.Close)

	client := api.NewOpenF1Client(fail.URL, 2*time.Second)
	t.Cleanup(func() { _ = client.Close() })
	srv := NewServer(client, 8080, st)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/driver/summary?year=2025&driver_number=1&source=auto", nil)
	rec := httptest.NewRecorder()
	srv.handleDriverSummary(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
	}
	var resp driverSummaryResponse
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Source != "local" {
		t.Errorf("source = %q, want local", resp.Source)
	}
	if resp.Enrichment != "limited" {
		t.Errorf("enrichment = %q, want limited", resp.Enrichment)
	}
	if rec.Header().Get(dataSourceHeader) != "local" || rec.Header().Get(dataFreshnessHeader) != "limited" {
		t.Errorf("limited metadata = %q/%q", rec.Header().Get(dataSourceHeader), rec.Header().Get(dataFreshnessHeader))
	}
	if resp.FullName != "Max Verstappen" {
		t.Errorf("full_name = %q, want local identity", resp.FullName)
	}
}

func TestHandleDriverSummarySourceLocalOnly(t *testing.T) {
	st := openTestStore(t)
	seedDriverSummaryStore(t, st)

	// Even with a broken OpenF1 client, source=local must succeed from the DB.
	fail := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "nope", http.StatusInternalServerError)
	}))
	t.Cleanup(fail.Close)
	client := api.NewOpenF1Client(fail.URL, time.Second)
	t.Cleanup(func() { _ = client.Close() })
	srv := NewServer(client, 8080, st)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/driver/summary?year=2025&driver_number=1&source=local", nil)
	rec := httptest.NewRecorder()
	srv.handleDriverSummary(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
	}
	if rec.Header().Get(dataSourceHeader) != "local" || rec.Header().Get(dataFreshnessHeader) != "local" {
		t.Fatalf("local metadata = %q/%q", rec.Header().Get(dataSourceHeader), rec.Header().Get(dataFreshnessHeader))
	}
}

func TestHandleRemoteDriverSummaryReportsIdentityAndRoundLimitations(t *testing.T) {
	tests := []struct {
		name           string
		driversOK      bool
		meetingHasRace bool
		wantEnrichment string
	}{
		{name: "missing identity", driversOK: false, meetingHasRace: true, wantEnrichment: "limited"},
		{name: "missing race round", driversOK: true, meetingHasRace: false, wantEnrichment: "full"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			upstream := championshipTestUpstream(t, tt.driversOK, tt.meetingHasRace)
			defer upstream.Close()
			client := api.NewOpenF1Client(upstream.URL, 2*time.Second)
			defer client.Close()
			server := NewServer(client, 0, nil)
			year := time.Now().Year()
			recorder := httptest.NewRecorder()
			request := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/driver/summary?year=%d&driver_number=1&source=openf1", year), nil)

			server.handleDriverSummary(recorder, request)
			if recorder.Code != http.StatusOK {
				t.Fatalf("status = %d body=%s", recorder.Code, recorder.Body.String())
			}
			var response driverSummaryResponse
			if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
				t.Fatal(err)
			}
			if response.Enrichment != tt.wantEnrichment {
				t.Fatalf("enrichment = %q, want %q", response.Enrichment, tt.wantEnrichment)
			}
			if recorder.Header().Get(dataSourceHeader) != "openf1" || recorder.Header().Get(dataFreshnessHeader) != "partial" {
				t.Fatalf("remote limitation metadata = %q/%q", recorder.Header().Get(dataSourceHeader), recorder.Header().Get(dataFreshnessHeader))
			}
		})
	}
}
