package audit

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

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestManager_NewManager(t *testing.T) {
	t.Run("disabled - no logger created", func(t *testing.T) {
		mgr, err := NewManager(&Config{Enabled: false})
		require.NoError(t, err)
		require.NotNil(t, mgr)
		assert.Nil(t, mgr.GetLogger())
		_ = mgr.Close()
	})

	t.Run("enabled - logger created", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:  true,
			FilePath: tmpFile,
		})
		require.NoError(t, err)
		require.NotNil(t, mgr)
		assert.NotNil(t, mgr.GetLogger())
		_ = mgr.Close()
	})

	t.Run("enabled but invalid config - error", func(t *testing.T) {
		mgr, err := NewManager(&Config{
			Enabled:  true,
			FilePath: "", // Invalid: empty path
		})
		require.Error(t, err)
		assert.Nil(t, mgr)
	})
}

func TestManager_OnAuditConfigChange(t *testing.T) {
	t.Run("disabled to disabled - no change", func(t *testing.T) {
		mgr, err := NewManager(&Config{Enabled: false})
		require.NoError(t, err)
		defer mgr.Close()

		assert.Nil(t, mgr.GetLogger())

		// Change config but still disabled
		err = mgr.OnAuditConfigChange(&Config{
			Enabled:            false,
			IncludeGetRequests: true,
		})
		require.NoError(t, err)
		assert.Nil(t, mgr.GetLogger())
	})

	t.Run("enabled to disabled - logger closed", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:  true,
			FilePath: tmpFile,
		})
		require.NoError(t, err)
		defer mgr.Close()

		assert.NotNil(t, mgr.GetLogger())

		// Disable audit
		err = mgr.OnAuditConfigChange(&Config{Enabled: false})
		require.NoError(t, err)
		assert.Nil(t, mgr.GetLogger())
	})

	t.Run("disabled to enabled - logger created", func(t *testing.T) {
		mgr, err := NewManager(&Config{Enabled: false})
		require.NoError(t, err)
		defer mgr.Close()

		assert.Nil(t, mgr.GetLogger())

		// Enable audit
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		err = mgr.OnAuditConfigChange(&Config{
			Enabled:  true,
			FilePath: tmpFile,
		})
		require.NoError(t, err)
		assert.NotNil(t, mgr.GetLogger())
	})

	t.Run("disabled to enabled with invalid config - error", func(t *testing.T) {
		mgr, err := NewManager(&Config{Enabled: false})
		require.NoError(t, err)
		defer mgr.Close()

		// Try to enable with invalid config
		err = mgr.OnAuditConfigChange(&Config{
			Enabled:  true,
			FilePath: "", // Invalid
		})
		require.Error(t, err)
		assert.Nil(t, mgr.GetLogger())
	})

	t.Run("enabled to enabled - hot config updated", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:            true,
			FilePath:           tmpFile,
			IncludeGetRequests: false,
		})
		require.NoError(t, err)
		defer mgr.Close()

		logger := mgr.GetLogger()
		require.NotNil(t, logger)

		// Update config while still enabled
		err = mgr.OnAuditConfigChange(&Config{
			Enabled:            true,
			FilePath:           tmpFile,
			IncludeGetRequests: true,
			FlushInterval:      500 * time.Millisecond,
		})
		require.NoError(t, err)

		// Logger should be the same instance (not recreated)
		assert.Same(t, logger, mgr.GetLogger())

		// Hot config should be updated
		assert.True(t, logger.hot.getIncludeGetRequests())
		assert.Equal(t, 500*time.Millisecond, logger.hot.getFlushInterval())
	})
}

func TestManager_RecordRequest(t *testing.T) {
	t.Run("disabled - no-op", func(t *testing.T) {
		mgr, err := NewManager(&Config{Enabled: false})
		require.NoError(t, err)
		defer mgr.Close()

		req := httptest.NewRequest(http.MethodPost, "/api/test", nil)

		// Should not panic or error
		mgr.RecordRequest(req, RequestRecordOptions{
			Start:        time.Now(),
			StatusCode:   http.StatusOK,
			OriginalPath: "/api/test",
		})
	})

	t.Run("enabled - delegates to logger", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:            true,
			FilePath:           tmpFile,
			IncludeGetRequests: true,
		})
		require.NoError(t, err)

		req := httptest.NewRequest(http.MethodGet, "/api/test", nil)

		// Should not panic
		mgr.RecordRequest(req, RequestRecordOptions{
			Start:        time.Now(),
			StatusCode:   http.StatusOK,
			OriginalPath: "/api/test",
		})

		// Explicitly close to ensure async write completes (no flaky sleep)
		err = mgr.Close()
		require.NoError(t, err)

		// Verify the audit log was actually written
		data, err := os.ReadFile(tmpFile)
		require.NoError(t, err)
		assert.Contains(t, string(data), "/api/test")
	})
}

func TestManager_AttachBodyCapture(t *testing.T) {
	t.Run("disabled - no wrap", func(t *testing.T) {
		mgr, err := NewManager(&Config{Enabled: false})
		require.NoError(t, err)
		defer mgr.Close()

		req := httptest.NewRequest(http.MethodPost, "/api/test", strings.NewReader("body"))
		updated := mgr.AttachBodyCapture(req)
		_, ok := updated.Body.(*BodyWrapper)
		assert.False(t, ok)
	})

	t.Run("enabled but MaxBodySize <= 0 - no wrap", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:     true,
			FilePath:    tmpFile,
			MaxBodySize: 0,
		})
		require.NoError(t, err)
		defer mgr.Close()

		req := httptest.NewRequest(http.MethodPost, "/api/test", strings.NewReader("body"))
		updated := mgr.AttachBodyCapture(req)
		_, ok := updated.Body.(*BodyWrapper)
		assert.False(t, ok)
	})

	t.Run("enabled and MaxBodySize > 0 - wrapped", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:     true,
			FilePath:    tmpFile,
			MaxBodySize: 256,
		})
		require.NoError(t, err)
		defer mgr.Close()

		req := httptest.NewRequest(http.MethodPost, "/api/test", strings.NewReader("body"))
		updated := mgr.AttachBodyCapture(req)
		_, ok := updated.Body.(*BodyWrapper)
		assert.True(t, ok)
	})
}

func TestManager_RecordRequestAutoBody(t *testing.T) {
	t.Run("status < 400 includes body", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:     true,
			FilePath:    tmpFile,
			MaxBodySize: 256,
		})
		require.NoError(t, err)

		body := `{"password":"secret","name":"alice"}`
		req := httptest.NewRequest(http.MethodPost, "/api/test", strings.NewReader(body))
		req = mgr.AttachBodyCapture(req)
		_, err = io.ReadAll(req.Body)
		require.NoError(t, err)

		mgr.RecordRequestAutoBody(req, RequestRecordOptions{
			Start:      time.Now(),
			StatusCode: http.StatusOK,
		})

		require.NoError(t, mgr.Close())

		data, err := os.ReadFile(tmpFile)
		require.NoError(t, err)
		lines := strings.Split(strings.TrimSpace(string(data)), "\n")
		require.Len(t, lines, 1)

		var entry Entry
		require.NoError(t, json.Unmarshal([]byte(lines[0]), &entry))

		var bodyMap map[string]interface{}
		require.NoError(t, json.Unmarshal([]byte(entry.RequestBody), &bodyMap))
		assert.Equal(t, redactedValue, bodyMap["password"])
		assert.Equal(t, "alice", bodyMap["name"])
	})

	t.Run("status >= 400 does not include body", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:     true,
			FilePath:    tmpFile,
			MaxBodySize: 256,
		})
		require.NoError(t, err)

		body := `{"password":"secret","name":"alice"}`
		req := httptest.NewRequest(http.MethodPost, "/api/test", strings.NewReader(body))
		req = mgr.AttachBodyCapture(req)
		_, err = io.ReadAll(req.Body)
		require.NoError(t, err)

		mgr.RecordRequestAutoBody(req, RequestRecordOptions{
			Start:      time.Now(),
			StatusCode: http.StatusBadRequest,
		})

		require.NoError(t, mgr.Close())

		data, err := os.ReadFile(tmpFile)
		require.NoError(t, err)
		lines := strings.Split(strings.TrimSpace(string(data)), "\n")
		require.Len(t, lines, 1)

		var entry Entry
		require.NoError(t, json.Unmarshal([]byte(lines[0]), &entry))
		assert.Equal(t, "", entry.RequestBody)
	})

	t.Run("hot reload MaxBodySize <= 0 disables body logging", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:     true,
			FilePath:    tmpFile,
			MaxBodySize: 256,
		})
		require.NoError(t, err)

		body := `{"password":"secret","name":"alice"}`
		req := httptest.NewRequest(http.MethodPost, "/api/test", strings.NewReader(body))
		req = mgr.AttachBodyCapture(req)
		_, err = io.ReadAll(req.Body)
		require.NoError(t, err)

		require.NoError(t, mgr.OnAuditConfigChange(&Config{
			Enabled:     true,
			FilePath:    tmpFile,
			MaxBodySize: 0,
		}))

		mgr.RecordRequestAutoBody(req, RequestRecordOptions{
			Start:      time.Now(),
			StatusCode: http.StatusOK,
		})

		require.NoError(t, mgr.Close())

		data, err := os.ReadFile(tmpFile)
		require.NoError(t, err)
		lines := strings.Split(strings.TrimSpace(string(data)), "\n")
		require.Len(t, lines, 1)

		var entry Entry
		require.NoError(t, json.Unmarshal([]byte(lines[0]), &entry))
		assert.Equal(t, "", entry.RequestBody)
	})
}

func TestManager_GetAuditSnapshot(t *testing.T) {
	t.Run("disabled - returns false, 0", func(t *testing.T) {
		mgr, err := NewManager(&Config{Enabled: false})
		require.NoError(t, err)
		defer mgr.Close()

		enabled, maxBodySize := mgr.GetAuditSnapshot()
		assert.False(t, enabled)
		assert.Equal(t, 0, maxBodySize)
	})

	t.Run("enabled - returns true and maxBodySize atomically", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:     true,
			FilePath:    tmpFile,
			MaxBodySize: 1024,
		})
		require.NoError(t, err)
		defer mgr.Close()

		enabled, maxBodySize := mgr.GetAuditSnapshot()
		assert.True(t, enabled)
		assert.Equal(t, 1024, maxBodySize)
	})

	t.Run("snapshot is consistent during config change", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:     true,
			FilePath:    tmpFile,
			MaxBodySize: 512,
		})
		require.NoError(t, err)
		defer mgr.Close()

		// Get snapshot - should be atomic
		enabled, maxBodySize := mgr.GetAuditSnapshot()
		assert.True(t, enabled)
		assert.Equal(t, 512, maxBodySize)

		// Update config
		err = mgr.OnAuditConfigChange(&Config{
			Enabled:     true,
			FilePath:    tmpFile,
			MaxBodySize: 2048,
		})
		require.NoError(t, err)

		// Get new snapshot
		enabled, maxBodySize = mgr.GetAuditSnapshot()
		assert.True(t, enabled)
		assert.Equal(t, 2048, maxBodySize)
	})
}

func TestManager_Close(t *testing.T) {
	t.Run("close with logger", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:  true,
			FilePath: tmpFile,
		})
		require.NoError(t, err)

		assert.NotNil(t, mgr.GetLogger())

		err = mgr.Close()
		require.NoError(t, err)
		assert.Nil(t, mgr.GetLogger())
	})

	t.Run("close without logger", func(t *testing.T) {
		mgr, err := NewManager(&Config{Enabled: false})
		require.NoError(t, err)

		err = mgr.Close()
		require.NoError(t, err)
	})

	t.Run("double close is safe", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "audit.log")
		mgr, err := NewManager(&Config{
			Enabled:  true,
			FilePath: tmpFile,
		})
		require.NoError(t, err)

		err = mgr.Close()
		require.NoError(t, err)

		// Second close should not error
		err = mgr.Close()
		require.NoError(t, err)
	})
}

func TestManager_ConcurrentDisableAndRecordRequest(t *testing.T) {
	tmpFile := filepath.Join(t.TempDir(), "audit.log")
	mgr, err := NewManager(&Config{
		Enabled:            true,
		FilePath:           tmpFile,
		IncludeGetRequests: true,
	})
	require.NoError(t, err)
	defer mgr.Close()

	req := httptest.NewRequest(http.MethodPost, "/api/test", nil)
	opts := RequestRecordOptions{
		Start:         time.Now(),
		StatusCode:    http.StatusOK,
		OriginalPath:  "/api/test",
		Body:          nil,
		BodyTruncated: false,
	}

	start := make(chan struct{})
	firstRecorded := make(chan struct{})
	var firstOnce sync.Once

	var wg sync.WaitGroup
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			for j := 0; j < 2000; j++ {
				mgr.RecordRequest(req, opts)
				firstOnce.Do(func() { close(firstRecorded) })
			}
		}()
	}

	disableErrCh := make(chan error, 1)
	go func() {
		<-firstRecorded
		disableErrCh <- mgr.OnAuditConfigChange(&Config{Enabled: false})
	}()

	close(start)
	wg.Wait()
	require.NoError(t, <-disableErrCh)

	// Re-enable after disable should work.
	err = mgr.OnAuditConfigChange(&Config{
		Enabled:            true,
		FilePath:           tmpFile,
		IncludeGetRequests: true,
	})
	require.NoError(t, err)

	mgr.RecordRequest(req, opts)
}
