package middleware

import (
	"bytes"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"kra/internal/conf"

	"github.com/gin-gonic/gin"
)

func TestAccessLogRejectsOversizedRequestBody(t *testing.T) {
	gin.SetMode(gin.TestMode)
	engine := gin.New()
	called := false
	engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
	engine.POST("/payload", func(c *gin.Context) { called = true })
	request := httptest.NewRequest(http.MethodPost, "/payload", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
	request.Header.Set("Content-Type", "application/json")
	response := httptest.NewRecorder()

	engine.ServeHTTP(response, request)
	if response.Code != http.StatusRequestEntityTooLarge || called {
		t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
	}
}

func TestAccessLogRejectsOversizedMultipartOnOrdinaryRoute(t *testing.T) {
	gin.SetMode(gin.TestMode)
	engine := gin.New()
	called := false
	engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
	engine.POST("/login", func(c *gin.Context) { called = true })
	request := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
	request.ContentLength = -1
	request.Header.Set("Content-Type", "multipart/form-data; boundary=test")
	response := httptest.NewRecorder()

	engine.ServeHTTP(response, request)
	if response.Code != http.StatusRequestEntityTooLarge || called {
		t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
	}
}

func TestAccessLogAllowsMediaLimitOnlyOnUploadRoute(t *testing.T) {
	gin.SetMode(gin.TestMode)
	engine := gin.New()
	called := false
	engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
	engine.POST("/api/fileUploadAndDownload/upload", func(c *gin.Context) {
		called = true
		_, _ = io.Copy(io.Discard, c.Request.Body)
	})
	request := httptest.NewRequest(http.MethodPost, "/api/fileUploadAndDownload/upload", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
	request.Header.Set("Content-Type", "multipart/form-data; boundary=test")
	response := httptest.NewRecorder()

	engine.ServeHTTP(response, request)
	if response.Code != http.StatusOK || !called {
		t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
	}
}

func TestAccessLogRedactsPaymentCallbackPayloadAndHeaders(t *testing.T) {
	gin.SetMode(gin.TestMode)
	var logs bytes.Buffer
	logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelInfo}))
	runtime := conf.NewRuntime(nil, &conf.AdminBackend{Zap: &conf.AdminBackend_Zap{AccessReqBody: true, AccessReqHeaders: true, AccessRespData: true}})
	engine := gin.New()
	engine.Use(AccessLog(runtime, logger, "test"))
	engine.POST("/api/payment/callback/:provider", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"body": "callback-response-secret"})
	})

	request := httptest.NewRequest(http.MethodPost, "/api/payment/callback/alipay?signature=query-secret", strings.NewReader("payment-body-secret"))
	request.Header.Set("Content-Type", "application/json; boundary=credential-secret")
	request.Header.Set("Authorization", "Bearer header-secret")
	request.Header.Set("X-Alipay-Signature", "signature-secret")
	response := httptest.NewRecorder()

	engine.ServeHTTP(response, request)
	logText := logs.String()
	for _, secret := range []string{"payment-body-secret", "query-secret", "header-secret", "signature-secret", "callback-response-secret", "credential-secret"} {
		if strings.Contains(logText, secret) {
			t.Fatalf("payment callback secret leaked into access log: %q in %s", secret, logText)
		}
	}
	for _, marker := range []string{"payment_callback=true", "payment_provider=alipay", "body_sha256=", "http_status=200"} {
		if !strings.Contains(logText, marker) {
			t.Fatalf("payment callback access summary missing %q: %s", marker, logText)
		}
	}
}

func TestAccessLogOmitsPaymentIntegrationConfigBody(t *testing.T) {
	gin.SetMode(gin.TestMode)
	var logs bytes.Buffer
	logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelInfo}))
	runtime := conf.NewRuntime(nil, &conf.AdminBackend{Zap: &conf.AdminBackend_Zap{AccessReqBody: true, AccessReqHeaders: true, AccessRespData: true}})
	engine := gin.New()
	engine.Use(AccessLog(runtime, logger, "test"))
	engine.PUT("/api/integration/configs/:kind/:provider", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0}) })

	body := `{"enabled":true,"config":{"key":"saobei-secret","certificate_blob":"certificate-secret","unknown_credential":"credential-secret"}}`
	request := httptest.NewRequest(http.MethodPut, "/api/integration/configs/payment/saobei", strings.NewReader(body))
	request.Header.Set("Content-Type", "application/json")
	response := httptest.NewRecorder()

	engine.ServeHTTP(response, request)
	logText := logs.String()
	for _, secret := range []string{"saobei-secret", "certificate-secret", "credential-secret"} {
		if strings.Contains(logText, secret) {
			t.Fatalf("payment configuration secret leaked into access log: %q in %s", secret, logText)
		}
	}
	if !strings.Contains(logText, "支付配置正文已省略") || !strings.Contains(logText, "body_sha256=") {
		t.Fatalf("payment configuration summary missing: %s", logText)
	}
}

func TestAccessLogRedactsWebSocketQueryToken(t *testing.T) {
	gin.SetMode(gin.TestMode)
	var logs bytes.Buffer
	logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelInfo}))
	runtime := conf.NewRuntime(nil, &conf.AdminBackend{})
	engine := gin.New()
	engine.Use(AccessLog(runtime, logger, "test"))
	engine.GET("/ws", func(c *gin.Context) { c.Status(http.StatusSwitchingProtocols) })

	request := httptest.NewRequest(http.MethodGet, "/ws?token=jwt-secret&room=orders", nil)
	response := httptest.NewRecorder()
	engine.ServeHTTP(response, request)

	logText := logs.String()
	if strings.Contains(logText, "jwt-secret") {
		t.Fatalf("websocket token leaked into access log: %s", logText)
	}
	if !strings.Contains(logText, "token=%2A%2A%2A") || !strings.Contains(logText, "room=orders") {
		t.Fatalf("redacted query missing expected fields: %s", logText)
	}
}
