package repository

import (
	"context"
	"fmt"
	"testing"

	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

	coremodel "security-center-backend/internal/model"
	"security-center-backend/internal/service/directory"
)

type auditLogTestUser struct {
	coremodel.Base
	Name       string  `gorm:"size:255"`
	Email      string  `gorm:"size:320"`
	EmployeeID *uint64 `gorm:"index"`
}

func (auditLogTestUser) TableName() string {
	return "users"
}

func TestGormAuditRepositoryListKeywordMatchesVisibleActorAndResourceText(t *testing.T) {
	ctx := context.Background()
	db := newAuditRepositoryTestDB(t)
	repo := NewAuditRepository(db)

	employee := directory.Employee{
		ID:    1,
		Name:  "Alice Chen",
		Email: "alice@example.com",
	}
	require.NoError(t, db.Create(&employee).Error)

	user := auditLogTestUser{
		Base:       coremodel.Base{ID: 10},
		Email:      "alice@example.com",
		Name:       "Alice Chen",
		EmployeeID: &employee.ID,
	}
	require.NoError(t, db.Create(&user).Error)

	targetUserID := user.ID
	primary := coremodel.AuditLog{
		Base:         coremodel.Base{ID: 100},
		ActorType:    "user",
		ActorUserID:  &targetUserID,
		ActorEmail:   user.Email,
		Action:       "update",
		ResourceType: "caasm_cloud_account",
		ResourceID:   "acct-123",
		IPAddress:    "10.0.0.10",
		UserAgent:    "Mozilla/5.0",
	}
	other := coremodel.AuditLog{
		Base:         coremodel.Base{ID: 101},
		ActorType:    "system",
		ActorEmail:   "system@example.com",
		Action:       "delete",
		ResourceType: "system_settings",
		ResourceID:   "setting-1",
		IPAddress:    "10.0.0.20",
		UserAgent:    "curl/8.0.1",
	}
	require.NoError(t, db.Create(&primary).Error)
	require.NoError(t, db.Create(&other).Error)

	items, total, err := repo.List(ctx, AuditFilter{Keyword: "Alice"}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(1), total)
	require.Len(t, items, 1)
	require.Equal(t, uint64(100), items[0].ID)
	require.NotNil(t, items[0].ActorUser)
	require.NotNil(t, items[0].ActorUser.Employee)
	require.Equal(t, "Alice Chen", items[0].ActorUser.Employee.Name)

	items, total, err = repo.List(ctx, AuditFilter{Keyword: "cloud account"}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(1), total)
	require.Len(t, items, 1)
	require.Equal(t, uint64(100), items[0].ID)
}

func TestGormAuditRepositoryListKeywordMatchesMetadataAndStateText(t *testing.T) {
	ctx := context.Background()
	db := newAuditRepositoryTestDB(t)
	repo := NewAuditRepository(db)

	log := coremodel.AuditLog{
		Base:         coremodel.Base{ID: 200},
		ActorType:    "user",
		ActorEmail:   "analyst@example.com",
		Action:       "update",
		ResourceType: "vulnerability",
		ResourceID:   "vuln-200",
		Metadata: coremodel.JSONB{
			"ticket": "INC-4242",
		},
		BeforeState: coremodel.JSONB{
			"status": "open",
		},
		AfterState: coremodel.JSONB{
			"status": "fixed",
		},
	}
	require.NoError(t, db.Create(&log).Error)

	for _, keyword := range []string{"inc-4242", "open", "fixed"} {
		items, total, err := repo.List(ctx, AuditFilter{Keyword: keyword}, 1, 20)
		require.NoError(t, err)
		require.Equal(t, int64(1), total, "keyword %s should match one row", keyword)
		require.Len(t, items, 1, "keyword %s should return the audit row", keyword)
		require.Equal(t, uint64(200), items[0].ID)
	}
}

func TestGormAuditRepositoryListKeywordMatchesMetadataBeforeAndAfterState(t *testing.T) {
	ctx := context.Background()
	db := newAuditRepositoryTestDB(t)
	repo := NewAuditRepository(db)

	user := auditLogTestUser{
		Base:  coremodel.Base{ID: 11},
		Email: "bob@example.com",
		Name:  "Bob Zhang",
	}
	require.NoError(t, db.Create(&user).Error)

	targetUserID := user.ID
	entry := coremodel.AuditLog{
		Base:         coremodel.Base{ID: 200},
		ActorType:    "user",
		ActorUserID:  &targetUserID,
		ActorEmail:   user.Email,
		Action:       "update",
		ResourceType: "vulnerability",
		ResourceID:   "vuln-9001",
		Metadata: coremodel.JSONB{
			"ticket_id": "SEC-777",
			"note":      "metadata keyword",
		},
		BeforeState: coremodel.JSONB{
			"verification_steps": "old validation flow",
			"poc_text":           "before proof",
		},
		AfterState: coremodel.JSONB{
			"verification_steps": "new validation flow",
			"poc_text":           "after proof",
		},
	}
	require.NoError(t, db.Create(&entry).Error)

	for _, keyword := range []string{"SEC-777", "old validation flow", "after proof"} {
		items, total, err := repo.List(ctx, AuditFilter{Keyword: keyword}, 1, 20)
		require.NoError(t, err)
		require.Equal(t, int64(1), total, "keyword %q should match exactly one audit row", keyword)
		require.Len(t, items, 1, "keyword %q should return exactly one row", keyword)
		require.Equal(t, uint64(200), items[0].ID)
	}
}

func TestGormAuditRepositoryListFiltersByActorType(t *testing.T) {
	ctx := context.Background()
	db := newAuditRepositoryTestDB(t)
	repo := NewAuditRepository(db)

	human := coremodel.AuditLog{
		Base:         coremodel.Base{ID: 300},
		ActorType:    "user",
		ActorEmail:   "alice@example.com",
		Action:       "GET",
		ResourceType: "caasm_asset",
	}
	agent := coremodel.AuditLog{
		Base:         coremodel.Base{ID: 301},
		ActorType:    "agent",
		ActorEmail:   "owner@example.com",
		Action:       "GET",
		ResourceType: "vuln_finding",
	}
	require.NoError(t, db.Create(&human).Error)
	require.NoError(t, db.Create(&agent).Error)

	// No actor_type filter returns both rows.
	_, total, err := repo.List(ctx, AuditFilter{}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(2), total)

	items, total, err := repo.List(ctx, AuditFilter{ActorType: "agent"}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(1), total)
	require.Len(t, items, 1)
	require.Equal(t, uint64(301), items[0].ID)

	items, total, err = repo.List(ctx, AuditFilter{ActorType: "user"}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(1), total)
	require.Len(t, items, 1)
	require.Equal(t, uint64(300), items[0].ID)
}

func newAuditRepositoryTestDB(t *testing.T) *gorm.DB {
	t.Helper()

	dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&directory.Employee{}, &auditLogTestUser{}, &coremodel.AuditLog{}))

	return db
}
