package mysql

import (
	"context"
	"encoding/json"
	"testing"
	"time"

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

	"github.com/tidbcloud/pov-service/internal/common/options"
	"github.com/tidbcloud/pov-service/internal/model"
	"github.com/tidbcloud/pov-service/internal/pkg/uuid"
)

func TestAuditLogRepo_Create(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping MySQL integration test in short mode")
	}

	db, cleanup := setupTestDB(t)
	defer cleanup()

	repo := NewAuditLogRepo(db)
	ctx := context.Background()

	// Test 1: Successful creation
	t.Run("Successful Creation", func(t *testing.T) {
		auditLog := model.AuditLog{
			OperatorMeta: model.OperatorMeta{
				Type: model.OperatorTypeUser,
				ID:   "user-123",
			},
			Type:       model.OperateTypeCreateCluster,
			ObjectID:   "cluster-abc",
			OperateAt:  time.Now().UTC(),
			DetailJSON: "{\"key\": \"value\"}",
		}

		err := repo.Create(ctx, auditLog)
		require.NoError(t, err)
		creates, err := repo.List(ctx, options.NewListAuditLogOption())
		require.NoError(t, err)
		createdLog := creates[0]
		assert.Equal(t, auditLog.OperatorMeta.Type, createdLog.OperatorMeta.Type)
		assert.Equal(t, auditLog.OperatorMeta.ID, createdLog.OperatorMeta.ID)
		assert.Equal(t, auditLog.Type, createdLog.Type)
		assert.Equal(t, auditLog.ObjectID, createdLog.ObjectID)
		assert.True(t, auditLog.OperateAt.Equal(createdLog.OperateAt) || auditLog.OperateAt.Before(createdLog.OperateAt)) // Allow slight time difference
		assert.Equal(t, auditLog.DetailJSON, createdLog.DetailJSON)
	})
}

func TestAuditLogRepo_List(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping MySQL integration test in short mode")
	}

	db, cleanup := setupTestDB(t)
	defer cleanup()

	repo := NewAuditLogRepo(db)
	ctx := context.Background()

	// Create some audit logs for testing list functionality
	logsToCreate := []model.AuditLog{
		{
			OperatorMeta: model.OperatorMeta{Type: model.OperatorTypeUser, ID: "user1"},
			Type:         model.OperateTypeCreateCluster,
			ObjectID:     "cluster1",
			OperateAt:    time.Now().Add(-5 * time.Hour).UTC(),
			DetailJSON:   "{}",
		},
		{
			OperatorMeta: model.OperatorMeta{Type: model.OperatorTypeService, ID: "service1"},
			Type:         model.OperateTypeCreateCluster,
			ObjectID:     "cluster2",
			OperateAt:    time.Now().Add(-3 * time.Hour).UTC(),
			DetailJSON:   "{\"change\": \"name\"}",
		},
		{
			OperatorMeta: model.OperatorMeta{Type: model.OperatorTypeUser, ID: "user2"},
			Type:         model.OperateTypeConfigTrafficConn,
			ObjectID:     "cluster3",
			OperateAt:    time.Now().Add(-1 * time.Hour).UTC(),
			DetailJSON:   "{}",
		},
		{
			OperatorMeta: model.OperatorMeta{Type: model.OperatorTypeUser, ID: "user1"},
			Type:         model.OperateTypeCreateWorkloadTest,
			ObjectID:     "workload1",
			OperateAt:    time.Now().Add(-2 * time.Hour).UTC(),
			DetailJSON:   "{}",
		},
	}

	for _, log := range logsToCreate {
		err := repo.Create(ctx, log)
		require.NoError(t, err)
	}

	// Test 1: List all logs without options
	t.Run("List all logs", func(t *testing.T) {
		listedLogs, err := repo.List(ctx, options.NewListAuditLogOption())
		require.NoError(t, err)
		assert.Len(t, listedLogs, len(logsToCreate))
		// Basic check that content is there, order might not be guaranteed without explicit ordering in query
		for _, created := range logsToCreate {
			found := false
			for _, listed := range listedLogs {
				if created.OperateAt.Truncate(time.Second) == listed.OperateAt.Truncate(time.Second) {
					found = true
					break
				}
			}
			assert.True(t, found, "Expected log not found: %v", created)
		}
	})

	// Test 2: Filter by OperatorID
	t.Run("Filter by OperatorID", func(t *testing.T) {
		opts := options.NewListAuditLogOption().AddFilter(options.OperatorID("user1"))
		listedLogs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		assert.Len(t, listedLogs, 2)
		for _, log := range listedLogs {
			assert.Equal(t, model.OperatorTypeUser, log.OperatorMeta.Type)
		}
	})

	// Test 3: Filter by OperateType
	t.Run("Filter by OperateType", func(t *testing.T) {
		opts := options.NewListAuditLogOption().AddFilter(options.OperateType(model.OperateTypeCreateCluster))
		listedLogs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		assert.Len(t, listedLogs, 1)
		assert.Equal(t, model.OperateTypeCreateCluster, listedLogs[0].Type)
	})

	// Test 4: Filter by ObjectID
	t.Run("Filter by ObjectID", func(t *testing.T) {
		opts := options.NewListAuditLogOption().AddFilter(options.ObjectID("cluster2"))
		listedLogs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		assert.Len(t, listedLogs, 1)
		assert.Equal(t, model.OperateTypeCreateWorkloadTest, listedLogs[0].Type)
	})

	// Test 5: Filter by multiple criteria (OperatorID and OperateType)
	t.Run("Filter by OperatorID and OperateType", func(t *testing.T) {
		opts := options.NewListAuditLogOption().AddFilter(options.OperatorID("user1")).AddFilter(options.OperateType(model.OperateTypeCreateWorkloadTest))
		listedLogs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		assert.Len(t, listedLogs, 1)
		assert.Equal(t, "user1", listedLogs[0].OperatorMeta.ID)
		assert.Equal(t, model.OperateTypeCreateWorkloadTest, listedLogs[0].Type)
		assert.Equal(t, "workload1", listedLogs[0].ObjectID)
	})

	// Test 6: No matching logs
	t.Run("No matching logs", func(t *testing.T) {
		opts := options.NewListAuditLogOption().AddFilter(options.OperatorID("nonexistent")).AddFilter(options.OperateType(model.OperateTypeCreateCluster))
		listedLogs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		assert.Len(t, listedLogs, 0)
	})

	// Test 7: Limit option
	t.Run("Limit option", func(t *testing.T) {
		opts := options.NewListAuditLogOption().SetLimit(2)
		listedLogs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		assert.Len(t, listedLogs, 2)
	})

	// Test 8: Offset option
	t.Run("Offset option", func(t *testing.T) {
		opts := options.NewListAuditLogOption().SetLimit(100).SetOffset(1)
		listedLogs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		assert.Len(t, listedLogs, len(logsToCreate)-1)
	})

	// Test 9: Combined Limit and Offset
	t.Run("Combined Limit and Offset", func(t *testing.T) {
		opts := options.NewListAuditLogOption().SetLimit(1).SetOffset(1)
		listedLogs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		assert.Len(t, listedLogs, 1)
	})

	// Test 10: OrderByOperateAtDesc
	t.Run("OrderByOperateAtDesc", func(t *testing.T) {
		opts := options.NewListAuditLogOption().SetOrderBy(options.OrderByOperateAt(options.OrderDesc))
		listedLogs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		require.Len(t, listedLogs, len(logsToCreate))
		// Verify descending order by OperateAt
		for i := 0; i < len(listedLogs)-1; i++ {
			assert.True(t, listedLogs[i].OperateAt.After(listedLogs[i+1].OperateAt) || listedLogs[i].OperateAt.Equal(listedLogs[i+1].OperateAt))
		}
	})

	// Test 11: OrderByOperateAtAsc
	t.Run("OrderByOperateAtAsc", func(t *testing.T) {
		opts := options.NewListAuditLogOption().SetOrderBy(options.OrderByOperateAt(options.OrderAsc))
		listedLogs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		require.Len(t, listedLogs, len(logsToCreate))
		// Verify ascending order by OperateAt
		for i := 0; i < len(listedLogs)-1; i++ {
			assert.True(t, listedLogs[i].OperateAt.Before(listedLogs[i+1].OperateAt) || listedLogs[i].OperateAt.Equal(listedLogs[i+1].OperateAt))
		}
	})
}

func TestAuditLogRepo_DBStoreTagsPreservation(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping MySQL integration test in short mode")
	}

	db, cleanup := setupTestDB(t)
	defer cleanup()

	repo := NewAuditLogRepo(db)
	ctx := context.Background()

	type ComplexDetail struct {
		EventID     string                 `json:"event_id"`
		Description string                 `json:"description"`
		Metadata    map[string]interface{} `json:"metadata"`
		Nested      struct {
			Key   string `json:"key"`
			Value int    `json:"value"`
		} `json:"nested"`
		SensitiveData string `json:"-"` // Should not be stored/retrieved
	}

	// Test 1: Successful creation and retrieval of complex JSON data
	t.Run("Successful creation with complex DetailJSON", func(t *testing.T) {
		complexDetail := ComplexDetail{
			EventID:     uuid.NewString(),
			Description: "This is a complex event detail.",
			Metadata: map[string]interface{}{
				"user_agent": "test-client",
				"ip_address": "192.168.1.1",
				"tags":       []string{"alpha", "beta"},
			},
			Nested: struct {
				Key   string `json:"key"`
				Value int    `json:"value"`
			}{
				Key:   "nested_key",
				Value: 123,
			},
			SensitiveData: "this should not be stored",
		}

		detailJSONBytes, err := json.Marshal(complexDetail)
		require.NoError(t, err)
		detailJSONString := string(detailJSONBytes)

		auditLog := model.AuditLog{
			OperatorMeta: model.OperatorMeta{
				Type: model.OperatorTypeUser,
				ID:   "complex-data-user",
			},
			Type:       model.OperateTypeCreateCluster,
			ObjectID:   "complex-object-123",
			OperateAt:  time.Now().UTC(),
			DetailJSON: detailJSONString,
		}

		err = repo.Create(ctx, auditLog)
		require.NoError(t, err)
		// assert.Equal(t, auditLog.DetailJSON, createdLog.DetailJSON)

		// Verify by listing and checking the detail
		opts := options.NewListAuditLogOption().AddFilter(options.ObjectID(auditLog.ObjectID))
		listedLogs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		assert.Len(t, listedLogs, 1)

		retrievedLog := listedLogs[0]
		// assert.Equal(t, auditLog.DetailJSON, retrievedLog.DetailJSON)

		// Unmarshal and verify the content
		var unmarshaledDetail ComplexDetail
		err = json.Unmarshal([]byte(retrievedLog.DetailJSON), &unmarshaledDetail)
		require.NoError(t, err)

		assert.Equal(t, complexDetail.EventID, unmarshaledDetail.EventID)
		assert.Equal(t, complexDetail.Description, unmarshaledDetail.Description)
		assert.Equal(t, complexDetail.Metadata["user_agent"], unmarshaledDetail.Metadata["user_agent"])
		assert.Equal(t, complexDetail.Metadata["ip_address"], unmarshaledDetail.Metadata["ip_address"])
		assert.ElementsMatch(t, complexDetail.Metadata["tags"], unmarshaledDetail.Metadata["tags"])
		assert.Equal(t, complexDetail.Nested.Key, unmarshaledDetail.Nested.Key)
		assert.Equal(t, complexDetail.Nested.Value, unmarshaledDetail.Nested.Value)

		// SensitiveData field should be empty as it has `json:"-"` tag
		assert.Empty(t, unmarshaledDetail.SensitiveData)
	})
}

func TestAuditLogRepo_ParallelIsolation(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping MySQL integration test in short mode")
	}

	t.Run("Parallel1", func(t *testing.T) {
		t.Parallel()

		db, cleanup := setupTestDB(t)
		defer cleanup()

		repo := NewAuditLogRepo(db)
		ctx := context.Background()

		auditLog := model.AuditLog{
			OperatorMeta: model.OperatorMeta{Type: model.OperatorTypeUser, ID: "parallel-user-1"},
			Type:         model.OperateTypeCreateCluster,
			ObjectID:     "parallel-cluster-1",
			OperateAt:    time.Now().UTC(),
			DetailJSON:   "{}",
		}
		err := repo.Create(ctx, auditLog)
		require.NoError(t, err)

		// This should only find our audit log
		opts := options.NewListAuditLogOption().AddFilter(options.OperatorID("parallel-user-1"))
		logs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		assert.Len(t, logs, 1)
		assert.Equal(t, auditLog.ObjectID, logs[0].ObjectID)
	})

	t.Run("Parallel2", func(t *testing.T) {
		t.Parallel()

		db, cleanup := setupTestDB(t)
		defer cleanup()

		repo := NewAuditLogRepo(db)
		ctx := context.Background()

		auditLog := model.AuditLog{
			OperatorMeta: model.OperatorMeta{Type: model.OperatorTypeUser, ID: "parallel-user-2"},
			Type:         model.OperateTypeCreateCluster,
			ObjectID:     "parallel-cluster-2",
			OperateAt:    time.Now().UTC(),
			DetailJSON:   "{}",
		}
		err := repo.Create(ctx, auditLog)
		require.NoError(t, err)

		// This should only find our audit log, not the one from Parallel1
		opts := options.NewListAuditLogOption().AddFilter(options.OperatorID("parallel-user-2"))
		logs, err := repo.List(ctx, opts)
		require.NoError(t, err)
		assert.Len(t, logs, 1)
		assert.Equal(t, auditLog.ObjectID, logs[0].ObjectID)
	})
}
