package config

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"

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

	"github.com/tidbcloud/service-proxy/pkg/audit"
)

func TestWatcher_NewWatcher(t *testing.T) {
	t.Run("valid config file", func(t *testing.T) {
		tmpDir := t.TempDir()
		configPath := filepath.Join(tmpDir, "config.yaml")
		configContent := `
server:
  addr: "0.0.0.0"
  port: 8080
audit:
  enabled: false
`
		require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0644))

		manager := NewManager(configPath)
		require.NoError(t, manager.Load())

		watcher, err := NewWatcher(manager)
		require.NoError(t, err)
		require.NotNil(t, watcher)
		assert.NotNil(t, watcher.lastAuditConfig)
		assert.Equal(t, defaultPollInterval, watcher.pollInterval)
		assert.True(t, watcher.hasConfigHash)
	})

	t.Run("nonexistent config file", func(t *testing.T) {
		manager := NewManager("/nonexistent/config.yaml")
		_ = manager.Load() // Will fail but we still try

		watcher, err := NewWatcher(manager)
		require.Error(t, err)
		assert.Nil(t, watcher)
	})

	t.Run("manager not loaded - returns clear error", func(t *testing.T) {
		// Create a valid config file
		tmpDir := t.TempDir()
		configPath := filepath.Join(tmpDir, "config.yaml")
		configContent := `
server:
  addr: "0.0.0.0"
  port: 8080
audit:
  enabled: false
`
		require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0644))

		// Create manager but DO NOT call Load()
		manager := NewManager(configPath)
		// Intentionally skipping manager.Load()

		// NewWatcher should return a clear error instead of nil pointer dereference
		watcher, err := NewWatcher(manager)
		require.Error(t, err)
		assert.Nil(t, watcher)
		assert.Contains(t, err.Error(), "config manager not initialized")
	})
}

func TestWatcher_RegisterAuditHandler(t *testing.T) {
	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "config.yaml")
	configContent := `
server:
  addr: "0.0.0.0"
  port: 8080
audit:
  enabled: false
`
	require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0644))

	manager := NewManager(configPath)
	require.NoError(t, manager.Load())

	watcher, err := NewWatcher(manager)
	require.NoError(t, err)

	// Register multiple handlers
	handler1 := &mockAuditHandler{}
	handler2 := &mockAuditHandler{}

	watcher.RegisterAuditHandler(handler1)
	watcher.RegisterAuditHandler(handler2)

	watcher.mu.RLock()
	assert.Len(t, watcher.auditHandlers, 2)
	watcher.mu.RUnlock()
}

func TestWatcher_Polling_AppliesOnlyOnContentChange(t *testing.T) {
	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "config.yaml")

	// Initial config (audit hot fields set but audit disabled => still valid).
	require.NoError(t, os.WriteFile(configPath, []byte(`
server:
  addr: "0.0.0.0"
  port: 8080
audit:
  enabled: false
  maxBodySize: 256
`), 0644))

	manager := NewManager(configPath)
	require.NoError(t, manager.Load())

	watcher, err := newWatcher(manager, 10*time.Millisecond)
	require.NoError(t, err)

	handler := &mockAuditHandler{}
	watcher.RegisterAuditHandler(handler)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go func() { _ = watcher.Run(ctx) }()

	// Change non-audit content: should reload manager config but NOT call audit handler.
	require.NoError(t, os.WriteFile(configPath, []byte(`
server:
  addr: "0.0.0.0"
  port: 8080
audit:
  enabled: false
  maxBodySize: 256
metrics:
  enabled: false
`), 0644))

	time.Sleep(50 * time.Millisecond)
	assert.Equal(t, 0, handler.Calls())

	// Change audit hot field: should trigger handler exactly once.
	require.NoError(t, os.WriteFile(configPath, []byte(`
server:
  addr: "0.0.0.0"
  port: 8080
audit:
  enabled: false
  maxBodySize: 512
`), 0644))

	require.Eventually(t, func() bool { return handler.Calls() == 1 }, time.Second, 10*time.Millisecond)

	cancel()
	require.NoError(t, watcher.Close())
}

func TestWatcher_Polling_NoRetryUntilContentChanges(t *testing.T) {
	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "config.yaml")

	require.NoError(t, os.WriteFile(configPath, []byte(`
server:
  addr: "0.0.0.0"
  port: 8080
audit:
  enabled: false
  maxBodySize: 256
`), 0644))

	manager := NewManager(configPath)
	require.NoError(t, manager.Load())

	watcher, err := newWatcher(manager, 10*time.Millisecond)
	require.NoError(t, err)

	handler := &mockAuditHandler{err: errors.New("boom")}
	watcher.RegisterAuditHandler(handler)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go func() { _ = watcher.Run(ctx) }()

	// First content change triggers exactly one call, even though handler returns error.
	require.NoError(t, os.WriteFile(configPath, []byte(`
server:
  addr: "0.0.0.0"
  port: 8080
audit:
  enabled: false
  maxBodySize: 512
`), 0644))
	require.Eventually(t, func() bool { return handler.Calls() == 1 }, time.Second, 10*time.Millisecond)

	// Wait several ticks: should NOT retry on same content.
	time.Sleep(100 * time.Millisecond)
	assert.Equal(t, 1, handler.Calls())

	// Next content change should trigger again.
	require.NoError(t, os.WriteFile(configPath, []byte(`
server:
  addr: "0.0.0.0"
  port: 8080
audit:
  enabled: false
  maxBodySize: 1024
`), 0644))
	require.Eventually(t, func() bool { return handler.Calls() == 2 }, time.Second, 10*time.Millisecond)

	cancel()
	require.NoError(t, watcher.Close())
}

func TestWatcher_auditConfigChanged(t *testing.T) {
	tests := []struct {
		name    string
		oldCfg  *audit.Config
		newCfg  *audit.Config
		changed bool
	}{
		{
			name:    "nil old config - always changed",
			oldCfg:  nil,
			newCfg:  &audit.Config{Enabled: true},
			changed: true,
		},
		{
			name:    "enabled changed true to false",
			oldCfg:  &audit.Config{Enabled: true},
			newCfg:  &audit.Config{Enabled: false},
			changed: true,
		},
		{
			name:    "enabled changed false to true",
			oldCfg:  &audit.Config{Enabled: false},
			newCfg:  &audit.Config{Enabled: true},
			changed: true,
		},
		{
			name:    "flush interval changed",
			oldCfg:  &audit.Config{FlushInterval: time.Second},
			newCfg:  &audit.Config{FlushInterval: 2 * time.Second},
			changed: true,
		},
		{
			name:    "include get requests changed",
			oldCfg:  &audit.Config{IncludeGetRequests: false},
			newCfg:  &audit.Config{IncludeGetRequests: true},
			changed: true,
		},
		{
			name:    "max body size changed",
			oldCfg:  &audit.Config{MaxBodySize: 256},
			newCfg:  &audit.Config{MaxBodySize: 512},
			changed: true,
		},
		{
			name:    "sensitive query params changed",
			oldCfg:  &audit.Config{SensitiveQueryParams: []string{"token"}},
			newCfg:  &audit.Config{SensitiveQueryParams: []string{"token", "apiKey"}},
			changed: true,
		},
		{
			name:    "sensitive body fields changed",
			oldCfg:  &audit.Config{SensitiveBodyFields: []string{"password"}},
			newCfg:  &audit.Config{SensitiveBodyFields: []string{"password", "secret"}},
			changed: true,
		},
		{
			name: "no change",
			oldCfg: &audit.Config{
				Enabled:              true,
				FlushInterval:        time.Second,
				IncludeGetRequests:   true,
				MaxBodySize:          256,
				SensitiveQueryParams: []string{"token"},
				SensitiveBodyFields:  []string{"password"},
			},
			newCfg: &audit.Config{
				Enabled:              true,
				FlushInterval:        time.Second,
				IncludeGetRequests:   true,
				MaxBodySize:          256,
				SensitiveQueryParams: []string{"token"},
				SensitiveBodyFields:  []string{"password"},
			},
			changed: false,
		},
		{
			name: "same slices different order - should be unchanged",
			oldCfg: &audit.Config{
				SensitiveQueryParams: []string{"a", "b"},
			},
			newCfg: &audit.Config{
				SensitiveQueryParams: []string{"b", "a"},
			},
			changed: false,
		},
		{
			name: "same slices different case - should be unchanged",
			oldCfg: &audit.Config{
				SensitiveQueryParams: []string{"Token", "ApiKey"},
			},
			newCfg: &audit.Config{
				SensitiveQueryParams: []string{"token", "apikey"},
			},
			changed: false,
		},
		{
			name: "duplicate elements - should be unchanged",
			oldCfg: &audit.Config{
				SensitiveQueryParams: []string{"Token", "token"},
			},
			newCfg: &audit.Config{
				SensitiveQueryParams: []string{"token"},
			},
			changed: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			watcher := &Watcher{
				lastAuditConfig: tt.oldCfg,
			}

			result := watcher.auditConfigChanged(tt.newCfg)
			assert.Equal(t, tt.changed, result)
		})
	}
}

func TestWatcher_copyAuditConfig(t *testing.T) {
	original := &audit.Config{
		Enabled:              true,
		FilePath:             "/var/log/audit.log",
		FlushInterval:        time.Second,
		IncludeGetRequests:   true,
		MaxBodySize:          256,
		SensitiveQueryParams: []string{"token", "apiKey"},
		SensitiveBodyFields:  []string{"password", "secret"},
	}

	copied := copyAuditConfig(original)

	// Verify values are equal
	assert.Equal(t, original.Enabled, copied.Enabled)
	assert.Equal(t, original.FilePath, copied.FilePath)
	assert.Equal(t, original.FlushInterval, copied.FlushInterval)
	assert.Equal(t, original.IncludeGetRequests, copied.IncludeGetRequests)
	assert.Equal(t, original.MaxBodySize, copied.MaxBodySize)
	assert.Equal(t, original.SensitiveQueryParams, copied.SensitiveQueryParams)
	assert.Equal(t, original.SensitiveBodyFields, copied.SensitiveBodyFields)

	// Verify slices are deep copied (not same underlying array)
	if len(original.SensitiveQueryParams) > 0 {
		original.SensitiveQueryParams[0] = "modified"
		assert.NotEqual(t, original.SensitiveQueryParams[0], copied.SensitiveQueryParams[0])
	}

	if len(original.SensitiveBodyFields) > 0 {
		original.SensitiveBodyFields[0] = "modified"
		assert.NotEqual(t, original.SensitiveBodyFields[0], copied.SensitiveBodyFields[0])
	}
}

func TestWatcher_copyAuditConfig_NilSlices(t *testing.T) {
	original := &audit.Config{
		Enabled:              true,
		SensitiveQueryParams: nil,
		SensitiveBodyFields:  nil,
	}

	copied := copyAuditConfig(original)

	assert.Nil(t, copied.SensitiveQueryParams)
	assert.Nil(t, copied.SensitiveBodyFields)
}

// mockAuditHandler is a test helper for verifying handler calls.
type mockAuditHandler struct {
	mu        sync.Mutex
	called    bool
	callCount int
	cfg       *audit.Config
	err       error
}

func (m *mockAuditHandler) OnAuditConfigChange(cfg *audit.Config) error {
	m.mu.Lock()
	m.called = true
	m.callCount++
	m.cfg = cfg
	m.mu.Unlock()
	return m.err
}

func (m *mockAuditHandler) Calls() int {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.callCount
}

func TestSliceSetEqual(t *testing.T) {
	tests := []struct {
		name  string
		a     []string
		b     []string
		equal bool
	}{
		{
			name:  "both nil",
			a:     nil,
			b:     nil,
			equal: true,
		},
		{
			name:  "both empty",
			a:     []string{},
			b:     []string{},
			equal: true,
		},
		{
			name:  "same elements same order",
			a:     []string{"a", "b", "c"},
			b:     []string{"a", "b", "c"},
			equal: true,
		},
		{
			name:  "same elements different order",
			a:     []string{"a", "b", "c"},
			b:     []string{"c", "a", "b"},
			equal: true,
		},
		{
			name:  "same elements different case",
			a:     []string{"Token", "ApiKey"},
			b:     []string{"token", "apikey"},
			equal: true,
		},
		{
			name:  "same elements mixed case and order",
			a:     []string{"TOKEN", "apikey", "Password"},
			b:     []string{"password", "APIKEY", "token"},
			equal: true,
		},
		{
			name:  "different lengths",
			a:     []string{"a", "b"},
			b:     []string{"a", "b", "c"},
			equal: false,
		},
		{
			name:  "different elements",
			a:     []string{"a", "b"},
			b:     []string{"a", "c"},
			equal: false,
		},
		{
			name:  "nil vs empty",
			a:     nil,
			b:     []string{},
			equal: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := sliceSetEqual(tt.a, tt.b)
			assert.Equal(t, tt.equal, result)
		})
	}
}
