package httpserver_test

import (
	"context"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"update_server/internal/apikeys"
	"update_server/internal/auth"
	"update_server/internal/config"
	"update_server/internal/db"
	httpserver "update_server/internal/http"
	"update_server/internal/releases"
	"update_server/internal/storage"
)

const testCSRFCookieName = "update_server_csrf"

func TestAdminLoginLogoutFlowProtectsRoutes(t *testing.T) {
	t.Parallel()

	router, cfg := newTestRouter(t)
	recorder := performRequest(t, router, http.MethodGet, "/admin", nil)
	if recorder.Code != http.StatusSeeOther {
		t.Fatalf("expected redirect for unauthenticated admin route, got %d", recorder.Code)
	}

	if location := recorder.Header().Get("Location"); location != "/admin/login" {
		t.Fatalf("expected login redirect, got %q", location)
	}

	loginForm := url.Values{
		"email":    {"admin@example.com"},
		"password": {"correct horse battery staple"},
		"next":     {"/admin"},
	}

	csrfCookie := ensureCSRFCookie(t, router)
	loginForm.Set("csrf_token", csrfCookie.Value)
	recorder = performRequest(t, router, http.MethodPost, "/admin/login", loginForm, csrfCookie)
	if recorder.Code != http.StatusSeeOther {
		t.Fatalf("expected login redirect, got %d", recorder.Code)
	}

	if location := recorder.Header().Get("Location"); location != "/admin" {
		t.Fatalf("expected admin redirect after login, got %q", location)
	}

	adminCookies := recorder.Result().Cookies()
	var sessionCookie *http.Cookie
	for _, cookie := range adminCookies {
		if cookie.Name == cfg.SessionCookieName {
			sessionCookie = cookie
			break
		}
	}

	if sessionCookie == nil || sessionCookie.Value == "" {
		t.Fatal("expected session cookie after successful login")
	}

	recorder = performRequest(t, router, http.MethodGet, "/admin", nil, sessionCookie)
	if recorder.Code != http.StatusOK {
		t.Fatalf("expected authenticated admin dashboard, got %d", recorder.Code)
	}

	if !strings.Contains(recorder.Body.String(), "Admin Dashboard") {
		t.Fatal("expected admin dashboard content in response body")
	}

	if !strings.Contains(recorder.Body.String(), "admin@example.com") {
		t.Fatal("expected authenticated admin email in dashboard response")
	}

	logoutCSRFCookie := ensureCSRFCookie(t, router, sessionCookie)
	recorder = performRequest(t, router, http.MethodPost, "/admin/logout", url.Values{
		"csrf_token": {logoutCSRFCookie.Value},
	}, sessionCookie, logoutCSRFCookie)
	if recorder.Code != http.StatusSeeOther {
		t.Fatalf("expected logout redirect, got %d", recorder.Code)
	}

	if location := recorder.Header().Get("Location"); location != "/admin/login" {
		t.Fatalf("expected login redirect after logout, got %q", location)
	}

	recorder = performRequest(t, router, http.MethodGet, "/admin", nil, sessionCookie)
	if recorder.Code != http.StatusSeeOther {
		t.Fatalf("expected invalidated session cookie to be rejected, got %d", recorder.Code)
	}

	if location := recorder.Header().Get("Location"); location != "/admin/login" {
		t.Fatalf("expected invalidated session redirect, got %q", location)
	}
}

func TestAdminLoginRejectsInvalidCredentials(t *testing.T) {
	t.Parallel()

	router, _ := newTestRouter(t)
	csrfCookie := ensureCSRFCookie(t, router)
	recorder := performRequest(t, router, http.MethodPost, "/admin/login", url.Values{
		"email":      {"admin@example.com"},
		"password":   {"definitely-wrong"},
		"next":       {"/admin"},
		"csrf_token": {csrfCookie.Value},
	}, csrfCookie)

	if recorder.Code != http.StatusUnauthorized {
		t.Fatalf("expected unauthorized login response, got %d", recorder.Code)
	}

	if !strings.Contains(recorder.Body.String(), "Invalid email or password.") {
		t.Fatal("expected invalid login message in response body")
	}
}

func newTestRouter(t *testing.T) (http.Handler, config.Config) {
	t.Helper()

	router, cfg, _ := newTestRouterWithStore(t)
	return router, cfg
}

func newTestRouterWithStore(t *testing.T) (http.Handler, config.Config, *db.Store) {
	return newTestRouterWithConfig(t, nil)
}

func newTestRouterWithConfig(t *testing.T, mutate func(*config.Config)) (http.Handler, config.Config, *db.Store) {
	t.Helper()

	store := newHTTPTestStore(t)
	t.Cleanup(func() {
		_ = store.Close()
	})

	artifactsDir := filepath.Join(t.TempDir(), "artifacts")
	cfg := config.Config{
		AppName:                  "Update Server",
		BaseURL:                  "http://127.0.0.1:8080",
		ArtifactsDir:             artifactsDir,
		TemplatesDir:             httpProjectPath(t, "web", "templates"),
		StaticDir:                httpProjectPath(t, "web", "static"),
		AdminEmail:               "admin@example.com",
		AdminPassword:            "correct horse battery staple",
		MaxUploadBytes:           8 << 20,
		SessionCookieName:        "update_server_session",
		CSRFCookieName:           testCSRFCookieName,
		SessionTTL:               24 * time.Hour,
		ReadTimeout:              30 * time.Second,
		ReadHeaderTimeout:        5 * time.Second,
		WriteTimeout:             60 * time.Second,
		IdleTimeout:              120 * time.Second,
		ShutdownTimeout:          10 * time.Second,
		MaxHeaderBytes:           1 << 20,
		LoginRateLimitPerMinute:  10,
		LoginRateLimitBurst:      5,
		ClientRateLimitPerMinute: 120,
		ClientRateLimitBurst:     60,
	}
	if mutate != nil {
		mutate(&cfg)
	}

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	renderer, err := httpserver.NewRenderer(cfg.TemplatesDir)
	if err != nil {
		t.Fatalf("create renderer: %v", err)
	}

	authService := auth.NewService(cfg, logger, store)
	apiKeyService := apikeys.NewService(store)
	if err := authService.EnsureBootstrapAdmin(context.Background()); err != nil {
		t.Fatalf("bootstrap admin: %v", err)
	}

	artifactStore, err := storage.NewLocal(cfg.ArtifactsDir)
	if err != nil {
		t.Fatalf("create artifact storage: %v", err)
	}

	releaseService := releases.NewService(store, artifactStore)
	return httpserver.NewRouter(cfg, logger, renderer, store, authService, apiKeyService, releaseService), cfg, store
}

func newHTTPTestStore(t *testing.T) *db.Store {
	t.Helper()

	sqlitePath := filepath.Join(t.TempDir(), "update-server.sqlite")
	database, err := db.Open(context.Background(), sqlitePath)
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}

	if err := db.Migrate(context.Background(), database, httpProjectPath(t, "migrations")); err != nil {
		_ = database.Close()
		t.Fatalf("migrate sqlite: %v", err)
	}

	return db.NewStore(database)
}

func httpProjectPath(t *testing.T, parts ...string) string {
	t.Helper()

	_, filename, _, ok := runtime.Caller(0)
	if !ok {
		t.Fatal("resolve caller path")
	}

	root := filepath.Join(filepath.Dir(filename), "..", "..")
	items := append([]string{root}, parts...)
	return filepath.Join(items...)
}

func performRequest(t *testing.T, handler http.Handler, method, target string, form url.Values, cookies ...*http.Cookie) *httptest.ResponseRecorder {
	t.Helper()

	var body io.Reader
	if form != nil {
		body = strings.NewReader(form.Encode())
	}

	req := httptest.NewRequest(method, target, body)
	req.RemoteAddr = "127.0.0.1:12345"
	if form != nil {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}

	for _, cookie := range cookies {
		req.AddCookie(cookie)
	}

	recorder := httptest.NewRecorder()
	handler.ServeHTTP(recorder, req)
	return recorder
}

func ensureCSRFCookie(t *testing.T, handler http.Handler, cookies ...*http.Cookie) *http.Cookie {
	t.Helper()

	for _, cookie := range cookies {
		if cookie != nil && cookie.Name == testCSRFCookieName && cookie.Value != "" {
			return cookie
		}
	}

	recorder := performRequest(t, handler, http.MethodGet, "/admin/login", nil, cookies...)
	if recorder.Code != http.StatusOK && recorder.Code != http.StatusSeeOther {
		t.Fatalf("expected csrf bootstrap request to succeed, got %d with body %s", recorder.Code, recorder.Body.String())
	}

	for _, cookie := range recorder.Result().Cookies() {
		if cookie.Name == testCSRFCookieName && cookie.Value != "" {
			return cookie
		}
	}

	t.Fatal("expected csrf cookie from admin bootstrap request")
	return nil
}
