package main

import (
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/service-proxy/pkg/audit"
)

func TestBuildAuditConfig(t *testing.T) {
	cfg := &Config{
		AuditEnabled:            true,
		AuditFilePath:           "/tmp/ops-audit.log",
		AuditIncludeGetRequests: true,
		AuditMaxBodySize:        256,
	}

	auditCfg := buildAuditConfig(cfg)

	require.True(t, auditCfg.Enabled)
	require.Equal(t, "/tmp/ops-audit.log", auditCfg.FilePath)
	require.True(t, auditCfg.IncludeGetRequests)
	require.Equal(t, 256, auditCfg.MaxBodySize)
	require.Contains(t, auditCfg.SensitiveBodyFields, "command")
	require.NotContains(t, auditCfg.SensitiveBodyFields, "encrypted_username")
	require.NotContains(t, auditCfg.SensitiveBodyFields, "encrypted_password")
}

func TestNewAuditManager_EnabledRequiresFilePath(t *testing.T) {
	_, err := newAuditManager(&Config{AuditEnabled: true})
	require.Error(t, err)
}

func TestHTTPAuditMiddleware_RecordsOriginalPrefixedPath(t *testing.T) {
	manager, auditFilePath := newTestHTTPAuditManager(t, &Config{
		AuditEnabled:            true,
		AuditIncludeGetRequests: false,
		AuditMaxBodySize:        0,
	})

	router := newTestHTTPAuditRouter(manager, true)
	router.POST("/cluster-agent/:tcns/api/v1/*any", func(c *gin.Context) {
		_, err := io.ReadAll(c.Request.Body)
		require.NoError(t, err)
		c.Status(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodPost, "/cluster-agent/tidb123/api/v1/clusters/test:executeCommand?token=abc", strings.NewReader(`{"command":"show"}`))
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	require.Equal(t, http.StatusOK, recorder.Code)

	require.NoError(t, manager.Close())
	entries := readAuditEntries(t, auditFilePath)
	require.Len(t, entries, 1)
	require.Equal(t, "/cluster-agent/tidb123/api/v1/clusters/test:executeCommand", entries[0].RequestURI)
	require.Equal(t, "token=%5BREDACTED%5D", entries[0].RequestParams)
}

func TestHTTPAuditMiddleware_RedactsCommandFieldOnly(t *testing.T) {
	manager, auditFilePath := newTestHTTPAuditManager(t, &Config{
		AuditEnabled:            true,
		AuditIncludeGetRequests: false,
		AuditMaxBodySize:        256,
	})

	router := newTestHTTPAuditRouter(manager, true)
	router.POST("/cluster-agent/:tcns/api/v1/*any", func(c *gin.Context) {
		_, err := io.ReadAll(c.Request.Body)
		require.NoError(t, err)
		c.Status(http.StatusOK)
	})

	requestBody := `{"command":"rm -rf /tmp","encrypted_username":"abc123","encrypted_password":"def456","safe":"ok"}`
	req := httptest.NewRequest(http.MethodPost, "/cluster-agent/tidb123/api/v1/clusters/test:executeCommand", strings.NewReader(requestBody))
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	require.Equal(t, http.StatusOK, recorder.Code)

	require.NoError(t, manager.Close())
	entries := readAuditEntries(t, auditFilePath)
	require.Len(t, entries, 1)

	bodyMap := map[string]any{}
	require.NoError(t, json.Unmarshal([]byte(entries[0].RequestBody), &bodyMap))
	require.Equal(t, "[REDACTED]", bodyMap["command"])
	require.Equal(t, "abc123", bodyMap["encrypted_username"])
	require.Equal(t, "def456", bodyMap["encrypted_password"])
	require.Equal(t, "ok", bodyMap["safe"])
}

func TestHTTPAuditMiddleware_DoesNotLogBodyForErrorResponses(t *testing.T) {
	manager, auditFilePath := newTestHTTPAuditManager(t, &Config{
		AuditEnabled:            true,
		AuditIncludeGetRequests: false,
		AuditMaxBodySize:        256,
	})

	router := newTestHTTPAuditRouter(manager, true)
	router.POST("/cluster-agent/:tcns/api/v1/*any", func(c *gin.Context) {
		_, err := io.ReadAll(c.Request.Body)
		require.NoError(t, err)
		c.AbortWithStatus(http.StatusBadRequest)
	})

	req := httptest.NewRequest(http.MethodPost, "/cluster-agent/tidb123/api/v1/clusters/test:executeCommand", strings.NewReader(`{"command":"show"}`))
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	require.Equal(t, http.StatusBadRequest, recorder.Code)

	require.NoError(t, manager.Close())
	entries := readAuditEntries(t, auditFilePath)
	require.Len(t, entries, 1)
	require.Empty(t, entries[0].RequestBody)
}

func TestHTTPAuditMiddleware_SkipsHealthzWhenHealthLoggingDisabled(t *testing.T) {
	manager, auditFilePath := newTestHTTPAuditManager(t, &Config{
		AuditEnabled:            true,
		AuditIncludeGetRequests: true,
		AuditMaxBodySize:        0,
	})

	router := newTestHTTPAuditRouter(manager, false)
	router.GET(healthzEndpoint, func(c *gin.Context) {
		c.Status(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodGet, healthzEndpoint, http.NoBody)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	require.Equal(t, http.StatusOK, recorder.Code)

	require.NoError(t, manager.Close())
	entries := readAuditEntries(t, auditFilePath)
	require.Empty(t, entries)
}

func TestHTTPAuditMiddleware_SkipsPrefixedHealthzWhenHealthLoggingDisabled(t *testing.T) {
	manager, auditFilePath := newTestHTTPAuditManager(t, &Config{
		AuditEnabled:            true,
		AuditIncludeGetRequests: true,
		AuditMaxBodySize:        0,
	})

	router := newTestHTTPAuditRouter(manager, false)
	router.GET("/cluster-agent/:tcns/api/v1/*any", func(c *gin.Context) {
		c.Status(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodGet, "/cluster-agent/tidb123/api/v1/healthz", http.NoBody)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	require.Equal(t, http.StatusOK, recorder.Code)

	require.NoError(t, manager.Close())
	entries := readAuditEntries(t, auditFilePath)
	require.Empty(t, entries)
}

func TestHTTPAuditMiddleware_SkipsPrefixedHealthzThroughNoRouteFallbackWhenHealthLoggingDisabled(t *testing.T) {
	manager, auditFilePath := newTestHTTPAuditManager(t, &Config{
		AuditEnabled:            true,
		AuditIncludeGetRequests: true,
		AuditMaxBodySize:        0,
	})

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(HTTPAuditMiddleware(manager, false))

	mux := http.NewServeMux()
	mux.HandleFunc(healthzEndpoint, func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	router.NoRoute(newGatewayFallbackHandler(mux))

	req := httptest.NewRequest(http.MethodGet, "/cluster-agent/tidb123/api/v1/healthz", http.NoBody)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	require.Equal(t, http.StatusOK, recorder.Code)

	require.NoError(t, manager.Close())
	entries := readAuditEntries(t, auditFilePath)
	require.Empty(t, entries)
}

func TestHTTPAuditMiddleware_PreservesFullBodyForDownstreamHandler(t *testing.T) {
	manager, auditFilePath := newTestHTTPAuditManager(t, &Config{
		AuditEnabled:            true,
		AuditIncludeGetRequests: false,
		AuditMaxBodySize:        256,
	})

	router := newTestHTTPAuditRouter(manager, true)
	largeBody := strings.Repeat("A", 1024)
	downstreamBody := ""
	router.POST("/cluster-agent/:tcns/api/v1/*any", func(c *gin.Context) {
		bodyBytes, err := io.ReadAll(c.Request.Body)
		require.NoError(t, err)
		downstreamBody = string(bodyBytes)
		c.Status(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodPost, "/cluster-agent/tidb123/api/v1/clusters/test:executeCommand", strings.NewReader(largeBody))
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, largeBody, downstreamBody)

	require.NoError(t, manager.Close())
	entries := readAuditEntries(t, auditFilePath)
	require.Len(t, entries, 1)
	require.NotEqual(t, largeBody, entries[0].RequestBody)
	require.True(t, strings.HasSuffix(entries[0].RequestBody, "...[truncated]"))
}

func TestHTTPAuditMiddleware_SkipsBusinessGetWhenIncludeGetDisabled(t *testing.T) {
	manager, auditFilePath := newTestHTTPAuditManager(t, &Config{
		AuditEnabled:            true,
		AuditIncludeGetRequests: false,
		AuditMaxBodySize:        0,
	})

	router := newTestHTTPAuditRouter(manager, true)
	router.GET("/api/v1/*any", func(c *gin.Context) {
		c.Status(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/clusters/test/topology", http.NoBody)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	require.Equal(t, http.StatusOK, recorder.Code)

	require.NoError(t, manager.Close())
	entries := readAuditEntries(t, auditFilePath)
	require.Empty(t, entries)
}

func newTestHTTPAuditRouter(manager *audit.Manager, logHealthCheck bool) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(HTTPAuditMiddleware(manager, logHealthCheck), ServePathStartFromAny)
	return router
}

func newTestHTTPAuditManager(t *testing.T, cfg *Config) (*audit.Manager, string) {
	t.Helper()

	testCfg := *cfg
	testCfg.AuditEnabled = true
	testCfg.AuditFilePath = filepath.Join(t.TempDir(), "ops-audit.log")

	manager, err := newAuditManager(&testCfg)
	require.NoError(t, err)
	return manager, testCfg.AuditFilePath
}

func readAuditEntries(t *testing.T, filePath string) []audit.Entry {
	t.Helper()

	data, err := os.ReadFile(filePath)
	require.NoError(t, err)
	trimmed := strings.TrimSpace(string(data))
	if trimmed == "" {
		return nil
	}

	lines := strings.Split(trimmed, "\n")
	entries := make([]audit.Entry, 0, len(lines))
	for _, line := range lines {
		entry := audit.Entry{}
		require.NoError(t, json.Unmarshal([]byte(line), &entry))
		entries = append(entries, entry)
	}

	return entries
}
