package api

import (
	"bytes"
	"compress/gzip"
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"

	"wucher/internal/config"
	"wucher/internal/shared/pkg/apperrorsx"
	"wucher/internal/transport/http/jsonapi"
	"wucher/internal/transport/http/response"
)

func testHTTPConfig() config.HTTPConfig {
	return config.HTTPConfig{
		CORSAllowOrigins:     "http://localhost:3000",
		CORSAllowMethods:     "GET,POST,PUT,PATCH,DELETE,OPTIONS",
		CORSAllowHeaders:     "Origin,Content-Type,Accept,Authorization",
		CORSExposeHeaders:    "X-Request-ID",
		CORSAllowCredentials: true,
		CORSMaxAge:           300,
		RateLimitMax:         100,
		RateLimitWindow:      time.Minute,
		RequestTimeout:       0,
		RequestTimeoutByPath: map[string]time.Duration{},
		HSTSMaxAge:           0,
	}
}

func TestUseDefaultMiddlewares_CORSHeaders(t *testing.T) {
	cfg := testHTTPConfig()
	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)
	app.Get("/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

	req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
	req.Header.Set("Origin", "http://localhost:3000")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "http://localhost:3000" {
		t.Fatalf("expected allow-origin http://localhost:3000, got %q", got)
	}
	if got := resp.Header.Get("Access-Control-Allow-Credentials"); got != "true" {
		t.Fatalf("expected allow-credentials true, got %q", got)
	}
	if got := resp.Header.Get("X-Frame-Options"); got != "DENY" {
		t.Fatalf("expected X-Frame-Options DENY, got %q", got)
	}
	if got := resp.Header.Get("X-Request-Id"); got == "" {
		t.Fatalf("expected request id header to be set")
	}
}

func TestUseDefaultMiddlewares_BlockTrace(t *testing.T) {
	cfg := testHTTPConfig()
	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)
	app.All("/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

	req, _ := http.NewRequest(http.MethodTrace, "/ping", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusMethodNotAllowed {
		t.Fatalf("expected 405, got %d", resp.StatusCode)
	}
}

func TestUseDefaultMiddlewares_RateLimiter(t *testing.T) {
	cfg := testHTTPConfig()
	cfg.RateLimitMax = 1
	cfg.RateLimitWindow = time.Minute

	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)
	app.Get("/limited", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

	firstReq, _ := http.NewRequest(http.MethodGet, "/limited", nil)
	firstResp, err := app.Test(firstReq)
	if err != nil {
		t.Fatalf("first request app.Test: %v", err)
	}
	if firstResp.StatusCode != http.StatusOK {
		t.Fatalf("expected first request 200, got %d", firstResp.StatusCode)
	}

	secondReq, _ := http.NewRequest(http.MethodGet, "/limited", nil)
	secondResp, err := app.Test(secondReq)
	if err != nil {
		t.Fatalf("second request app.Test: %v", err)
	}
	if secondResp.StatusCode != http.StatusTooManyRequests {
		t.Fatalf("expected second request 429, got %d", secondResp.StatusCode)
	}
	if got := secondResp.Header.Get("Retry-After"); got == "" {
		t.Fatalf("expected Retry-After header on 429 response")
	}
}

func TestUseDefaultMiddlewares_EndpointRateLimiter(t *testing.T) {
	cfg := testHTTPConfig()
	cfg.RateLimitMax = 100
	cfg.RateLimitByEndpoint = map[string]config.RateLimitRule{
		"POST /auth/login": {
			Max:    1,
			Window: time.Minute,
		},
	}

	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)
	app.Post("/auth/login", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
	app.Post("/auth/refresh", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

	req1, _ := http.NewRequest(http.MethodPost, "/auth/login", nil)
	resp1, err := app.Test(req1)
	if err != nil {
		t.Fatalf("first login request app.Test: %v", err)
	}
	if resp1.StatusCode != http.StatusOK {
		t.Fatalf("expected first login request 200, got %d", resp1.StatusCode)
	}

	req2, _ := http.NewRequest(http.MethodPost, "/auth/login", nil)
	resp2, err := app.Test(req2)
	if err != nil {
		t.Fatalf("second login request app.Test: %v", err)
	}
	if resp2.StatusCode != http.StatusTooManyRequests {
		t.Fatalf("expected second login request 429, got %d", resp2.StatusCode)
	}

	otherReq, _ := http.NewRequest(http.MethodPost, "/auth/refresh", nil)
	otherResp, err := app.Test(otherReq)
	if err != nil {
		t.Fatalf("other endpoint request app.Test: %v", err)
	}
	if otherResp.StatusCode != http.StatusOK {
		t.Fatalf("expected other endpoint request 200, got %d", otherResp.StatusCode)
	}
}

func TestUseDefaultMiddlewares_WildcardOriginDisablesCredentials(t *testing.T) {
	cfg := testHTTPConfig()
	cfg.CORSAllowOrigins = "*"
	cfg.CORSAllowCredentials = true

	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)
	app.Get("/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

	req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
	req.Header.Set("Origin", "http://localhost:3000")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "*" {
		t.Fatalf("expected allow-origin *, got %q", got)
	}
	if got := resp.Header.Get("Access-Control-Allow-Credentials"); got != "" {
		t.Fatalf("expected allow-credentials to be omitted, got %q", got)
	}
}

func TestUseDefaultMiddlewares_RequestContextDetachedFromShutdown(t *testing.T) {
	cfg := testHTTPConfig()
	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)
	app.Get("/ctx", func(c *fiber.Ctx) error {
		if c.UserContext() == nil {
			return c.SendStatus(http.StatusInternalServerError)
		}
		if c.UserContext().Done() != nil {
			return c.SendStatus(http.StatusInternalServerError)
		}
		return c.SendStatus(http.StatusOK)
	})

	req, _ := http.NewRequest(http.MethodGet, "/ctx", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

func TestUseDefaultMiddlewares_CompressesLargeJSONWhenGzipAccepted(t *testing.T) {
	cfg := testHTTPConfig()
	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)

	largePayload := map[string]string{
		"data": strings.Repeat("x", 8*1024),
	}
	app.Get("/large-json", func(c *fiber.Ctx) error {
		return c.JSON(largePayload)
	})

	req, _ := http.NewRequest(http.MethodGet, "/large-json", nil)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	if got := resp.Header.Get("Content-Encoding"); got != "gzip" {
		t.Fatalf("expected Content-Encoding gzip, got %q", got)
	}
	gz, err := gzip.NewReader(resp.Body)
	if err != nil {
		t.Fatalf("gzip reader: %v", err)
	}
	defer gz.Close()

	var payload map[string]string
	if err := json.NewDecoder(gz).Decode(&payload); err != nil {
		t.Fatalf("decode json: %v", err)
	}
	if payload["data"] != largePayload["data"] {
		t.Fatalf("unexpected payload body")
	}
}

func TestUseDefaultMiddlewares_RequestTimeoutExceeded(t *testing.T) {
	cfg := testHTTPConfig()
	cfg.RequestTimeout = 10 * time.Millisecond

	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)
	app.Get("/slow", func(c *fiber.Ctx) error {
		select {
		case <-time.After(50 * time.Millisecond):
			return c.SendStatus(http.StatusOK)
		case <-c.UserContext().Done():
			return c.SendStatus(http.StatusRequestTimeout)
		}
	})

	req, _ := http.NewRequest(http.MethodGet, "/slow", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusRequestTimeout {
		t.Fatalf("expected 408, got %d", resp.StatusCode)
	}
}

func TestUseDefaultMiddlewares_RequestTimeoutNormalRequestUnaffected(t *testing.T) {
	cfg := testHTTPConfig()
	cfg.RequestTimeout = 200 * time.Millisecond

	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)
	app.Get("/fast", func(c *fiber.Ctx) error {
		return c.SendStatus(http.StatusOK)
	})

	req, _ := http.NewRequest(http.MethodGet, "/fast", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

func TestUseDefaultMiddlewares_RequestTimeoutPerEndpointOverride(t *testing.T) {
	cfg := testHTTPConfig()
	cfg.RequestTimeout = 10 * time.Millisecond
	cfg.RequestTimeoutByPath = map[string]time.Duration{
		"GET /slow": 120 * time.Millisecond,
	}

	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)
	app.Get("/slow", func(c *fiber.Ctx) error {
		select {
		case <-time.After(40 * time.Millisecond):
			return c.SendStatus(http.StatusOK)
		case <-c.UserContext().Done():
			return c.SendStatus(http.StatusRequestTimeout)
		}
	})

	req, _ := http.NewRequest(http.MethodGet, "/slow", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200 with override timeout, got %d", resp.StatusCode)
	}
}

func TestUseDefaultMiddlewares_RequestTimeoutPerEndpointOverrideDynamicRoute(t *testing.T) {
	cfg := testHTTPConfig()
	cfg.RequestTimeout = 10 * time.Millisecond
	cfg.RequestTimeoutByPath = map[string]time.Duration{
		"GET /users/:id": 120 * time.Millisecond,
	}

	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)
	app.Get("/users/:id", func(c *fiber.Ctx) error {
		select {
		case <-time.After(40 * time.Millisecond):
			return c.SendStatus(http.StatusOK)
		case <-c.UserContext().Done():
			return c.SendStatus(http.StatusRequestTimeout)
		}
	})

	req, _ := http.NewRequest(http.MethodGet, "/users/123", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200 with dynamic route override timeout, got %d", resp.StatusCode)
	}
}

func TestHTTPAccessLogMiddlewareIncludesErrorDetail(t *testing.T) {
	var logBuf bytes.Buffer
	prev := slog.Default()
	slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, &slog.HandlerOptions{})))
	t.Cleanup(func() {
		slog.SetDefault(prev)
	})

	app := fiber.New()
	app.Use(httpAccessLogMiddleware())
	app.Get("/bad", func(c *fiber.Ctx) error {
		return response.WriteErrors(c, http.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Title:  "Validation error",
			Detail: "name is required",
		}})
	})

	req, _ := http.NewRequest(http.MethodGet, "/bad", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", resp.StatusCode)
	}
	body, _ := io.ReadAll(resp.Body)
	if strings.Contains(string(body), "name is required") {
		t.Fatalf("expected sanitized response detail, got %s", string(body))
	}
	if !strings.Contains(string(body), `"detail":"validation failed"`) {
		t.Fatalf("expected generic response detail, got %s", string(body))
	}
	if !strings.Contains(logBuf.String(), "name is required") {
		t.Fatalf("expected access log to include original detail, got %s", logBuf.String())
	}
}

func TestHTTPAccessLogMiddlewareIncludesResponseBodyOnServerError(t *testing.T) {
	var logBuf bytes.Buffer
	prev := slog.Default()
	slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, &slog.HandlerOptions{})))
	t.Cleanup(func() {
		slog.SetDefault(prev)
	})

	app := fiber.New()
	app.Use(httpAccessLogMiddleware())
	app.Get("/boom", func(c *fiber.Ctx) error {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
			"errors": []fiber.Map{{
				"status": "500",
				"title":  "Update failed",
				"detail": "db deadlock on takeover update",
			}},
		})
	})

	req, _ := http.NewRequest(http.MethodGet, "/boom", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusInternalServerError {
		t.Fatalf("expected 500, got %d", resp.StatusCode)
	}
	if !strings.Contains(logBuf.String(), "db deadlock on takeover update") {
		t.Fatalf("expected access log to include response body detail, got %s", logBuf.String())
	}
	if !strings.Contains(logBuf.String(), "response_body") {
		t.Fatalf("expected access log to include response_body field, got %s", logBuf.String())
	}
}

func TestHTTPAccessLogMiddlewareIncludesInternalErrorDetail(t *testing.T) {
	var logBuf bytes.Buffer
	prev := slog.Default()
	slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, &slog.HandlerOptions{})))
	t.Cleanup(func() {
		slog.SetDefault(prev)
	})

	app := fiber.New()
	app.Use(httpAccessLogMiddleware())
	app.Get("/boom", func(c *fiber.Ctx) error {
		c.Locals(apperrorsx.ContextKeyHTTPErrorInternalDetail, "action=UpdateTakeover | error=reserve update failed | chain=*errors.errorString: reserve update failed")
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
			"errors": []fiber.Map{{
				"status": "500",
				"title":  "Update failed",
				"detail": "internal server error",
			}},
		})
	})

	req, _ := http.NewRequest(http.MethodGet, "/boom", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusInternalServerError {
		t.Fatalf("expected 500, got %d", resp.StatusCode)
	}
	if !strings.Contains(logBuf.String(), "internal_detail") {
		t.Fatalf("expected access log to include internal_detail field, got %s", logBuf.String())
	}
	if !strings.Contains(logBuf.String(), "reserve update failed") {
		t.Fatalf("expected access log to include internal error detail, got %s", logBuf.String())
	}
}

func TestUseDefaultMiddlewares_HTTPMetricsUsesRoutePatternLabels(t *testing.T) {
	cfg := testHTTPConfig()
	app := fiber.New()
	UseDefaultMiddlewares(app, cfg)
	app.Get("/users/:id", func(c *fiber.Ctx) error {
		return c.SendStatus(http.StatusCreated)
	})
	app.Get("/metrics", func(c *fiber.Ctx) error {
		metricsHandler(c.Context())
		return nil
	})

	beforeCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/users/:id", "201")
	beforeHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/users/:id", "201")

	req, _ := http.NewRequest(http.MethodGet, "/users/123", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("expected 201, got %d", resp.StatusCode)
	}

	afterCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/users/:id", "201")
	afterHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/users/:id", "201")
	if afterCounter <= beforeCounter {
		t.Fatalf("expected counter to increase, before=%v after=%v", beforeCounter, afterCounter)
	}
	if afterHistogram <= beforeHistogram {
		t.Fatalf("expected histogram count to increase, before=%v after=%v", beforeHistogram, afterHistogram)
	}

	metricsReq, _ := http.NewRequest(http.MethodGet, "/metrics", nil)
	metricsResp, err := app.Test(metricsReq)
	if err != nil {
		t.Fatalf("metrics app.Test: %v", err)
	}
	if metricsResp.StatusCode != http.StatusOK {
		t.Fatalf("expected /metrics status 200, got %d", metricsResp.StatusCode)
	}
	body, err := io.ReadAll(metricsResp.Body)
	if err != nil {
		t.Fatalf("read /metrics body: %v", err)
	}
	metricsBody := string(body)
	if !strings.Contains(metricsBody, "http_requests_total") {
		t.Fatalf("expected http_requests_total in /metrics output")
	}
	if !strings.Contains(metricsBody, "http_request_duration_seconds") {
		t.Fatalf("expected http_request_duration_seconds in /metrics output")
	}
	if !strings.Contains(metricsBody, "path=\"/users/:id\"") {
		t.Fatalf("expected route-pattern path label in /metrics output")
	}

	beforeMetricsCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/metrics", "200")
	beforeMetricsHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/metrics", "200")
	metricsReq2, _ := http.NewRequest(http.MethodGet, "/metrics", nil)
	metricsResp2, err := app.Test(metricsReq2)
	if err != nil {
		t.Fatalf("metrics second app.Test: %v", err)
	}
	if metricsResp2.StatusCode != http.StatusOK {
		t.Fatalf("expected second /metrics status 200, got %d", metricsResp2.StatusCode)
	}
	afterMetricsCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/metrics", "200")
	afterMetricsHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/metrics", "200")
	if afterMetricsCounter != beforeMetricsCounter {
		t.Fatalf("expected /metrics to be excluded from counter, before=%v after=%v", beforeMetricsCounter, afterMetricsCounter)
	}
	if afterMetricsHistogram != beforeMetricsHistogram {
		t.Fatalf("expected /metrics to be excluded from histogram, before=%v after=%v", beforeMetricsHistogram, afterMetricsHistogram)
	}
}

func TestNormalizeHTTPMethod(t *testing.T) {
	tests := []struct {
		name   string
		input  string
		output string
	}{
		{name: "get", input: "GET", output: "GET"},
		{name: "lowercase", input: "post", output: "POST"},
		{name: "trimmed", input: "  put  ", output: "PUT"},
		{name: "empty", input: "", output: "UNKNOWN"},
		{name: "custom", input: "GETT", output: "OTHER"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := normalizeHTTPMethod(tt.input); got != tt.output {
				t.Fatalf("expected %q, got %q", tt.output, got)
			}
		})
	}
}

func metricCounterValue(t *testing.T, vec *prometheus.CounterVec, labels ...string) float64 {
	t.Helper()
	m := &dto.Metric{}
	if err := vec.WithLabelValues(labels...).Write(m); err != nil {
		t.Fatalf("read counter metric: %v", err)
	}
	return m.GetCounter().GetValue()
}

func metricHistogramCount(t *testing.T, vec *prometheus.HistogramVec, labels ...string) uint64 {
	t.Helper()
	observer, err := vec.GetMetricWithLabelValues(labels...)
	if err != nil {
		t.Fatalf("load histogram metric: %v", err)
	}
	metric, ok := observer.(prometheus.Metric)
	if !ok {
		t.Fatalf("histogram observer does not implement prometheus.Metric")
	}
	m := &dto.Metric{}
	if err := metric.Write(m); err != nil {
		t.Fatalf("read histogram metric: %v", err)
	}
	if m.GetHistogram() == nil {
		t.Fatalf("histogram metric not available for labels %v", labels)
	}
	return m.GetHistogram().GetSampleCount()
}
