package web

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"regexp"
	"strings"
	"testing"
)

// stubPinger satisfies the web.Pinger interface that Plan 01-03 will declare.
// Local to this test file — never reachable from production code.
type stubPinger struct {
	err error
}

func (s stubPinger) Ping(ctx context.Context) error { return s.err }

func TestHealthz_OK(t *testing.T) {
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/healthz", nil)

	// HealthzHandler takes no args — pure liveness, no DB ping (D-12).
	HealthzHandler().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d; want 200", rec.Code)
	}
	if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") {
		t.Errorf("Content-Type = %q; want application/json", ct)
	}
	body := rec.Body.String()
	if !strings.Contains(body, `"status":"ok"`) {
		t.Errorf("body missing status:ok; got: %s", body)
	}
	// Liveness endpoint must NOT expose db field (T-07-01: no internal info in responses).
	if strings.Contains(body, `"db"`) {
		t.Errorf("liveness body must not contain db field; got: %s", body)
	}
}

// TestHealthz_Down is deleted — new HealthzHandler has no failure mode (D-12).

func TestReadyz_OK(t *testing.T) {
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/readyz", nil)

	ReadyzHandler(stubPinger{err: nil}).ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d; want 200", rec.Code)
	}
	if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") {
		t.Errorf("Content-Type = %q; want application/json", ct)
	}
	body := rec.Body.String()
	if !strings.Contains(body, `"status":"ok"`) {
		t.Errorf("body missing status:ok; got: %s", body)
	}
	if !strings.Contains(body, `"db":"ok"`) {
		t.Errorf("body missing db:ok; got: %s", body)
	}
}

func TestReadyz_Down(t *testing.T) {
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/readyz", nil)

	ReadyzHandler(stubPinger{err: errors.New("conn refused")}).ServeHTTP(rec, req)

	if rec.Code != http.StatusServiceUnavailable {
		t.Fatalf("status = %d; want 503", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, `"status":"degraded"`) {
		t.Errorf("body missing status:degraded; got: %s", body)
	}
	if !strings.Contains(body, `"db":"down"`) {
		t.Errorf("body missing db:down; got: %s", body)
	}
}

// TestIndex_UnauthRedirects verifies that an unauthenticated GET / now
// redirects to /login (AUTH-05: / is protected behind RequireAuth).
// This replaces the Phase 1 TestIndex_RendersHxGet test which assumed /
// was public. The HTMX demo content is tested by
// TestProtected_HomeAuthRendersUserEmail in handlers_auth_test.go.
func TestIndex_UnauthRedirects(t *testing.T) {
	router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, DiscussionDeps{}, PlanningDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
	if err != nil {
		t.Fatalf("NewRouter: %v", err)
	}
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/", nil)

	router.ServeHTTP(rec, req)

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

func TestDemoTime_Fragment(t *testing.T) {
	router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, DiscussionDeps{}, PlanningDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
	if err != nil {
		t.Fatalf("NewRouter: %v", err)
	}
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/demo/time", nil)

	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d; want 200", rec.Code)
	}
	if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
		t.Errorf("Content-Type = %q; want text/html", ct)
	}
	body := rec.Body.String()
	if strings.Contains(body, "<html") {
		t.Errorf("body looks like full page (contains <html); want fragment\nbody: %s", body)
	}
	iso := regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)
	if !iso.MatchString(body) {
		t.Errorf("body missing ISO-8601 UTC timestamp; got: %s", body)
	}
}

func TestRequestID_HeaderSet(t *testing.T) {
	router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, DiscussionDeps{}, PlanningDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
	if err != nil {
		t.Fatalf("NewRouter: %v", err)
	}
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/healthz", nil)

	router.ServeHTTP(rec, req)

	rid := rec.Header().Get("X-Request-ID")
	if rid == "" {
		t.Fatal("X-Request-ID header is empty")
	}
	uuidv4 := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
	if !uuidv4.MatchString(rid) {
		t.Errorf("X-Request-ID %q does not match UUIDv4 regex", rid)
	}
}

func TestSlog_HandlerSwitch(t *testing.T) {
	var prodBuf bytes.Buffer
	prodHandler := NewSlogHandler("production", &prodBuf)
	slog.New(prodHandler).Info("x")
	var prodLine map[string]any
	if err := json.Unmarshal(bytes.TrimSpace(prodBuf.Bytes()), &prodLine); err != nil {
		t.Fatalf("production handler should emit JSON; parse error: %v\nbuf: %s", err, prodBuf.String())
	}
	if prodLine["msg"] != "x" {
		t.Errorf("production log msg = %v; want \"x\"", prodLine["msg"])
	}

	var devBuf bytes.Buffer
	devHandler := NewSlogHandler("development", &devBuf)
	slog.New(devHandler).Info("x")
	var devLine map[string]any
	if err := json.Unmarshal(bytes.TrimSpace(devBuf.Bytes()), &devLine); err == nil {
		t.Errorf("development handler should NOT emit JSON; got parseable JSON: %s", devBuf.String())
	}
}
