package web

import (
	"context"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"
	"time"

	"backend/internal/auth"
	"backend/internal/db/sqlc"

	"github.com/jackc/pgx/v5/pgtype"
)

func newPlanningTestRouter(q *sqlc.Queries, store *auth.Store, now func() time.Time) http.Handler {
	authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
	tabloDeps := TablosDeps{Queries: q}
	taskDeps := TasksDeps{Queries: q}
	etapeDeps := EtapesDeps{Queries: q}
	eventDeps := EventsDeps{Queries: q}
	discussionDeps := DiscussionDeps{Queries: q}
	planningDeps := PlanningDeps{Queries: q, Now: now}
	router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, etapeDeps, eventDeps, discussionDeps, planningDeps, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
	if err != nil {
		panic("newPlanningTestRouter: " + err.Error())
	}
	return router
}

func fixedPlanningNow() time.Time {
	return time.Date(2026, time.May, 16, 9, 0, 0, 0, time.Local)
}

func insertPlanningTestTablo(t *testing.T, ctx context.Context, q *sqlc.Queries, user sqlc.User, title, color string) sqlc.Tablo {
	t.Helper()
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       title,
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{String: color, Valid: color != ""},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	return tablo
}

func TestPlanningRequiresAuth(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newPlanningTestRouter(q, store, fixedPlanningNow)

	req := httptest.NewRequest(http.MethodGet, "/planning", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusSeeOther {
		t.Fatalf("GET /planning status = %d; want 303", rec.Code)
	}
	if got := rec.Header().Get("Location"); got != "/login" {
		t.Fatalf("GET /planning Location = %q; want /login", got)
	}
}

func TestPlanningDefaultsToUpcomingFourteenDays(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newPlanningTestRouter(q, store, fixedPlanningNow)

	user := preInsertUser(t, ctx, q, "planningdefault@example.com", "correct-horse-12")
	tablo := insertPlanningTestTablo(t, ctx, q, user, "Default Window", "#2563eb")
	insertEventTestEvent(t, ctx, q, tablo.ID, "Starts Today", "2026-05-16", "09:30", "", "", "")
	insertEventTestEvent(t, ctx, q, tablo.ID, "Ends Window", "2026-05-29", "10:00", "", "", "")
	insertEventTestEvent(t, ctx, q, tablo.ID, "Outside Window", "2026-05-30", "10:00", "", "", "")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/planning", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET /planning status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	for _, want := range []string{"Planning", "May 16 - May 29, 2026", "Starts Today", "Ends Window"} {
		if !strings.Contains(body, want) {
			t.Fatalf("planning default range missing %q; body: %.800s", want, body)
		}
	}
	if strings.Contains(body, "Outside Window") {
		t.Fatalf("planning default range included event outside 14-day window; body: %.800s", body)
	}
}

func TestPlanningListsOwnedEventsChronologically(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newPlanningTestRouter(q, store, fixedPlanningNow)

	user := preInsertUser(t, ctx, q, "planningorder@example.com", "correct-horse-12")
	blueTablo := insertPlanningTestTablo(t, ctx, q, user, "Blue Launch", "#123abc")
	redTablo := insertPlanningTestTablo(t, ctx, q, user, "Red Studio", "")
	insertEventTestEvent(t, ctx, q, redTablo.ID, "Gamma", "2026-05-18", "09:00", "", "Studio", "Hidden description")
	insertEventTestEvent(t, ctx, q, blueTablo.ID, "Beta", "2026-05-17", "11:00", "12:00", "Office", "")
	insertEventTestEvent(t, ctx, q, blueTablo.ID, "Alpha", "2026-05-17", "11:00", "", "", "")
	insertEventTestEvent(t, ctx, q, blueTablo.ID, "Early", "2026-05-17", "09:30", "", "HQ", "")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/planning?start=2026-05-16", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET /planning status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	assertBodyOrder(t, body, "Early", "Alpha", "Beta", "Gamma")
	for _, want := range []string{"May 17, 2026", "May 18, 2026", "09:30", "11:00", "11:00-12:00", "Blue Launch", "#123abc", "Red Studio", "Office", "HQ", "/tablos/" + blueTablo.ID.String() + "/events?month=2026-05", "/tablos/" + redTablo.ID.String() + "/events?month=2026-05"} {
		if !strings.Contains(body, want) {
			t.Fatalf("planning agenda missing %q; body: %.1200s", want, body)
		}
	}
	if strings.Contains(body, "Hidden description") {
		t.Fatalf("planning agenda rendered description snippet; body: %.1200s", body)
	}
}

func TestPlanningDoesNotLeakOtherUsersEvents(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newPlanningTestRouter(q, store, fixedPlanningNow)

	owner := preInsertUser(t, ctx, q, "planningowner@example.com", "correct-horse-12")
	other := preInsertUser(t, ctx, q, "planningother@example.com", "correct-horse-12")
	ownedTablo := insertPlanningTestTablo(t, ctx, q, owner, "Owned Planning", "#2563eb")
	foreignTablo := insertPlanningTestTablo(t, ctx, q, other, "Foreign Planning", "#b91c1c")
	insertEventTestEvent(t, ctx, q, ownedTablo.ID, "Owned Event", "2026-05-20", "09:30", "", "", "")
	insertEventTestEvent(t, ctx, q, foreignTablo.ID, "Foreign Event", "2026-05-20", "09:30", "", "", "")
	sessionCookie := sessionCookieForUser(t, ctx, store, owner)

	req := httptest.NewRequest(http.MethodGet, "/planning?start=2026-05-16", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET /planning status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	if !strings.Contains(body, "Owned Event") {
		t.Fatalf("planning agenda missing owned event; body: %.800s", body)
	}
	if strings.Contains(body, "Foreign Event") {
		t.Fatalf("planning agenda leaked foreign event; body: %.800s", body)
	}
}

func TestPlanningNavigationLinks(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newPlanningTestRouter(q, store, fixedPlanningNow)

	user := preInsertUser(t, ctx, q, "planningnav@example.com", "correct-horse-12")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/planning?start=2026-05-16", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET /planning status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	for _, want := range []string{"Previous 14 days", "Today", "Next 14 days", "start=2026-05-02", "href=\"/planning\"", "start=2026-05-30"} {
		if !strings.Contains(body, want) {
			t.Fatalf("planning navigation missing %q; body: %.1000s", want, body)
		}
	}
}

func TestPlanningEmptyState(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newPlanningTestRouter(q, store, fixedPlanningNow)

	user := preInsertUser(t, ctx, q, "planningempty@example.com", "correct-horse-12")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/planning?start=2026-05-16", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET /planning status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	for _, want := range []string{"No events in this range", "Use the navigation controls to browse another 14-day window.", "Previous 14 days", "Today", "Next 14 days"} {
		if !strings.Contains(body, want) {
			t.Fatalf("planning empty state missing %q; body: %.1000s", want, body)
		}
	}
}

func TestPlanningInvalidStartFallsBackToToday(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newPlanningTestRouter(q, store, fixedPlanningNow)

	user := preInsertUser(t, ctx, q, "planninginvalid@example.com", "correct-horse-12")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/planning?start=not-a-date", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET /planning invalid start status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	if !strings.Contains(rec.Body.String(), "May 16 - May 29, 2026") {
		t.Fatalf("invalid start did not fall back to today range; body: %.1000s", rec.Body.String())
	}
}

func assertBodyOrder(t *testing.T, body string, values ...string) {
	t.Helper()
	last := -1
	for _, value := range values {
		idx := strings.Index(body, value)
		if idx == -1 {
			t.Fatalf("body missing %q; body: %.1200s", value, body)
		}
		if idx <= last {
			t.Fatalf("body order mismatch: %q appeared at %d after prior index %d; body: %.1200s", value, idx, last, body)
		}
		last = idx
	}
}
