package web

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

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

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

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

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

	ctx := context.Background()

	rows, err := pool.Query(ctx, `
		SELECT column_name
		FROM information_schema.columns
		WHERE table_schema = current_schema()
		  AND table_name = 'etapes'
	`)
	if err != nil {
		t.Fatalf("query etapes columns: %v", err)
	}
	defer rows.Close()

	etapeColumns := make(map[string]bool)
	for rows.Next() {
		var name string
		if err := rows.Scan(&name); err != nil {
			t.Fatalf("scan etapes column: %v", err)
		}
		etapeColumns[name] = true
	}
	if err := rows.Err(); err != nil {
		t.Fatalf("iterate etapes columns: %v", err)
	}
	for _, forbidden := range []string{"parent_id", "parent_etape_id", "etape_id"} {
		if etapeColumns[forbidden] {
			t.Fatalf("etapes has nested-etape column %q; want one-level etapes only", forbidden)
		}
	}

	var nullable string
	if err := pool.QueryRow(ctx, `
		SELECT is_nullable
		FROM information_schema.columns
		WHERE table_schema = current_schema()
		  AND table_name = 'tasks'
		  AND column_name = 'etape_id'
	`).Scan(&nullable); err != nil {
		t.Fatalf("query tasks.etape_id nullability: %v", err)
	}
	if nullable != "YES" {
		t.Fatalf("tasks.etape_id is_nullable = %q; want YES", nullable)
	}

	var deleteRule string
	if err := pool.QueryRow(ctx, `
		SELECT rc.delete_rule
		FROM information_schema.table_constraints AS tc
		JOIN information_schema.key_column_usage AS kcu
		  ON tc.constraint_schema = kcu.constraint_schema
		 AND tc.constraint_name = kcu.constraint_name
		JOIN information_schema.referential_constraints AS rc
		  ON tc.constraint_schema = rc.constraint_schema
		 AND tc.constraint_name = rc.constraint_name
		WHERE tc.table_schema = current_schema()
		  AND tc.table_name = 'tasks'
		  AND tc.constraint_type = 'FOREIGN KEY'
		  AND kcu.column_name = 'etape_id'
	`).Scan(&deleteRule); err != nil {
		t.Fatalf("query tasks.etape_id delete rule: %v", err)
	}
	if deleteRule != "SET NULL" {
		t.Fatalf("tasks.etape_id delete rule = %q; want SET NULL", deleteRule)
	}
}

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

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

	user := preInsertUser(t, ctx, q, "etapecreate@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Etape Create Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}

	cookieVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/tasks", []*http.Cookie{sessionCookie})

	form := url.Values{
		"title": {"Design"},
		"_csrf": {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/etapes", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("POST /tablos/{id}/etapes status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	for _, want := range []string{"Design", "Unassigned", "All"} {
		if !strings.Contains(body, want) {
			t.Errorf("response body missing %q; body: %.500s", want, body)
		}
	}
}

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

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

	user := preInsertUser(t, ctx, q, "tasketapecreate@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Task Etape Create Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Design",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}

	cookieVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/tasks", []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":    {"Assigned Task"},
		"status":   {"todo"},
		"etape_id": {etape.ID.String()},
		"_csrf":    {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("POST /tablos/{id}/tasks status = %d; want 200", rec.Code)
	}
	tasks, err := q.ListTasksByTablo(ctx, tablo.ID)
	if err != nil {
		t.Fatalf("ListTasksByTablo: %v", err)
	}
	if len(tasks) != 1 {
		t.Fatalf("task count = %d; want 1", len(tasks))
	}
	if !tasks[0].EtapeID.Valid || tasks[0].EtapeID.Bytes != etape.ID {
		t.Fatalf("task etape_id = %+v; want %s", tasks[0].EtapeID, etape.ID)
	}
}

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

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

	user := preInsertUser(t, ctx, q, "etapefilter@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Etape Filter Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Design",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}
	_, err = q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Assigned Task",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
		EtapeID:     pgtype.UUID{Bytes: etape.ID, Valid: true},
	})
	if err != nil {
		t.Fatalf("InsertTask assigned: %v", err)
	}
	_, err = q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Unassigned Task",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    200,
		EtapeID:     pgtype.UUID{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTask unassigned: %v", err)
	}

	cookieVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/tasks?etape="+etape.ID.String(), nil)
	req.Header.Set("HX-Request", "true")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET /tablos/{id}/tasks?etape status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	for _, col := range []string{"To do", "In progress", "In review", "Done"} {
		if !strings.Contains(body, col) {
			t.Errorf("filtered kanban missing column header %q", col)
		}
	}
	if !strings.Contains(body, "Assigned Task") {
		t.Errorf("filtered body missing assigned task; body: %.500s", body)
	}
	if strings.Contains(body, "Unassigned Task") {
		t.Errorf("filtered body includes unassigned task; body: %.500s", body)
	}
}

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

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

	user := preInsertUser(t, ctx, q, "etapeupdate@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Etape Update Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Draft",
		Description: pgtype.Text{String: "Old", Valid: true},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}

	cookieVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/tasks", []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":       {"Design"},
		"description": {"Ready for build"},
		"_csrf":       {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/etapes/"+etape.ID.String(), strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("POST /tablos/{id}/etapes/{etape_id} status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "Design") {
		t.Fatalf("response body missing updated title; body: %.500s", body)
	}
	updated, err := q.GetEtapeByID(ctx, sqlc.GetEtapeByIDParams{ID: etape.ID, TabloID: tablo.ID})
	if err != nil {
		t.Fatalf("GetEtapeByID: %v", err)
	}
	if updated.Title != "Design" || !updated.Description.Valid || updated.Description.String != "Ready for build" {
		t.Fatalf("updated etape = %+v; want new title and description", updated)
	}
}

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

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

	user := preInsertUser(t, ctx, q, "etapedelete@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Etape Delete Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Design",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}
	task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Survives",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
		EtapeID:     pgtype.UUID{Bytes: etape.ID, Valid: true},
	})
	if err != nil {
		t.Fatalf("InsertTask: %v", err)
	}

	cookieVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/tasks", []*http.Cookie{sessionCookie})

	form := url.Values{"_csrf": {csrfToken}}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/etapes/"+etape.ID.String()+"/delete", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("POST delete etape status = %d; want 200", rec.Code)
	}
	remaining, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: tablo.ID})
	if err != nil {
		t.Fatalf("GetTaskByID after etape delete: %v", err)
	}
	if remaining.EtapeID.Valid {
		t.Fatalf("task etape_id valid after etape delete; want null")
	}
}

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

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

	user := preInsertUser(t, ctx, q, "etapereorder@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Etape Reorder Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	first, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "First",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape first: %v", err)
	}
	second, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Second",
		Description: pgtype.Text{Valid: false},
		Position:    200,
	})
	if err != nil {
		t.Fatalf("InsertEtape second: %v", err)
	}

	cookieVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/tasks", []*http.Cookie{sessionCookie})

	form := url.Values{
		"etape_id": {second.ID.String(), first.ID.String()},
		"_csrf":    {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/etapes/reorder", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("POST reorder etapes status = %d; want 200", rec.Code)
	}
	etapes, err := q.ListEtapesByTablo(ctx, tablo.ID)
	if err != nil {
		t.Fatalf("ListEtapesByTablo: %v", err)
	}
	if len(etapes) != 2 || etapes[0].ID != second.ID || etapes[1].ID != first.ID {
		t.Fatalf("etape order = %+v; want second then first", etapes)
	}
}

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

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

	owner := preInsertUser(t, ctx, q, "etapeowner@example.com", "correct-horse-12")
	nonOwner := preInsertUser(t, ctx, q, "etapenonowner@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      owner.ID,
		Title:       "Owned Elsewhere",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Private",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}

	cookieVal, _, err := store.Create(ctx, nonOwner.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
	csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie})

	cases := []struct {
		name string
		path string
		form url.Values
	}{
		{
			name: "update",
			path: "/tablos/" + tablo.ID.String() + "/etapes/" + etape.ID.String(),
			form: url.Values{"title": {"Nope"}, "_csrf": {csrfToken}},
		},
		{
			name: "delete",
			path: "/tablos/" + tablo.ID.String() + "/etapes/" + etape.ID.String() + "/delete",
			form: url.Values{"_csrf": {csrfToken}},
		},
		{
			name: "reorder",
			path: "/tablos/" + tablo.ID.String() + "/etapes/reorder",
			form: url.Values{"etape_id": {etape.ID.String()}, "_csrf": {csrfToken}},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodPost, tc.path, strings.NewReader(tc.form.Encode()))
			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
			req.Header.Set("HX-Request", "true")
			for _, c := range csrfCookies {
				req.AddCookie(c)
			}
			rec := httptest.NewRecorder()
			router.ServeHTTP(rec, req)
			if rec.Code != http.StatusNotFound {
				t.Fatalf("%s status = %d; want 404", tc.name, rec.Code)
			}
		})
	}
}
