package kb

import (
	"context"
	"reflect"
	"strings"
	"sync"
	"testing"
	"time"

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

func TestStoreCarriesUADashboardPublicationIntoNewSourceRevision(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Date(2026, 7, 23, 6, 15, 0, 0, time.UTC)
	first := createUADashboardRevision(t, store, now)
	_, swapped, err := store.PublishUADashboard(ctx, uaDashboardPublishParams(first, "audit-first", "dashboard-old", now.Add(time.Minute)))
	require.NoError(t, err)
	require.True(t, swapped)

	next, created, err := store.AdvanceSourceRevision(ctx, AdvanceSourceRevisionParams{
		ExpectedCurrentSourceCommitSHA: "source-sha-1",
		EnforceExpectedCurrent:         true,
		Revision: SourceRevision{
			ID: "rev-2", WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SourceID: "src-1",
			SourceCommitSHA: "source-sha-2", InsightsCommitSHA: "insights-sha-2", FreshnessStatus: "current",
			CreatedAt: now.Add(2 * time.Minute), UpdatedAt: now.Add(2 * time.Minute),
		},
	})
	require.NoError(t, err)
	require.True(t, created)
	require.Equal(t, "dashboard-old", next.UADashboardCommitSHA)
	require.Equal(t, `{"files":[]}`, next.UADashboardManifestJSON)
	require.Equal(t, now.Add(time.Minute), *next.UADashboardPublishedAt)
	current, err := store.GetCurrentSourceRevision(ctx, "ws-1", "kb-1", "src-1")
	require.NoError(t, err)
	require.Equal(t, "rev-2", current.ID)
	require.Equal(t, "dashboard-old", current.UADashboardCommitSHA)
}

func TestSourceRevisionHasNoJSONTags(t *testing.T) {
	typeOfRevision := reflect.TypeOf(SourceRevision{})
	for i := 0; i < typeOfRevision.NumField(); i++ {
		field := typeOfRevision.Field(i)
		_, present := field.Tag.Lookup("json")
		require.False(t, present, "SourceRevision.%s must not define a JSON tag", field.Name)
	}
}

func TestStorePublishesUADashboardWithFencedCASAndAudit(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Date(2026, 7, 23, 6, 30, 0, 0, time.UTC)
	revision := createUADashboardRevision(t, store, now)

	published, swapped, err := store.PublishUADashboard(ctx, PublishUADashboardParams{
		WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SourceID: "src-1",
		RevisionID: revision.ID, SourceCommitSHA: revision.SourceCommitSHA, PreviousCommitSHA: "",
		DashboardCommitSHA: "dashboard-sha-1", ManifestJSON: `{"files":[]}`, PublishedAt: now.Add(time.Minute),
		AuditEvent: AuditEvent{ID: "audit-1", WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", ActorID: "user-1", Action: "kb.source.dashboard.import", ResourceType: "source", ResourceID: "src-1", Summary: "Imported dashboard", Details: `{}`, CreatedAt: now.Add(time.Minute)},
	})
	require.NoError(t, err)
	require.True(t, swapped)
	require.Equal(t, "dashboard-sha-1", published.UADashboardCommitSHA)
	require.Equal(t, `{"files":[]}`, published.UADashboardManifestJSON)
	require.Equal(t, now.Add(time.Minute), *published.UADashboardPublishedAt)

	events, err := store.ListAuditEvents(ctx, "ws-1", "kb-1", 1, 20)
	require.NoError(t, err)
	require.Len(t, events, 1)
	require.Equal(t, "kb.source.dashboard.import", events[0].Action)
}

func TestStoreEnforcesUADashboardManifestByteCapacityBeforeWrite(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Date(2026, 7, 23, 6, 45, 0, 0, time.UTC)
	revision := createUADashboardRevision(t, store, now)

	maxParams := uaDashboardPublishParams(revision, "audit-max", "dashboard-max", now.Add(time.Minute))
	maxParams.ManifestJSON = strings.Repeat("x", UADashboardManifestMaxBytes)
	published, swapped, err := store.PublishUADashboard(ctx, maxParams)
	require.NoError(t, err)
	require.True(t, swapped)
	require.Len(t, []byte(published.UADashboardManifestJSON), UADashboardManifestMaxBytes)

	tooLarge := uaDashboardPublishParams(published, "audit-too-large", "dashboard-too-large", now.Add(2*time.Minute))
	tooLarge.ManifestJSON = strings.Repeat("x", UADashboardManifestMaxBytes+1)
	_, swapped, err = store.PublishUADashboard(ctx, tooLarge)
	require.ErrorContains(t, err, "manifest exceeds 16384 bytes")
	require.False(t, swapped)
	current, loadErr := store.GetCurrentSourceRevision(ctx, "ws-1", "kb-1", "src-1")
	require.NoError(t, loadErr)
	require.Equal(t, "dashboard-max", current.UADashboardCommitSHA)
	require.Len(t, []byte(current.UADashboardManifestJSON), UADashboardManifestMaxBytes)
}

func TestStoreRejectsStaleUADashboardPublicationFences(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*PublishUADashboardParams)
	}{
		{name: "previous dashboard", mutate: func(p *PublishUADashboardParams) { p.PreviousCommitSHA = "stale" }},
		{name: "source commit", mutate: func(p *PublishUADashboardParams) { p.SourceCommitSHA = "stale" }},
		{name: "revision", mutate: func(p *PublishUADashboardParams) { p.RevisionID = "rev-stale" }},
		{name: "workspace", mutate: func(p *PublishUADashboardParams) { p.WorkspaceID = "ws-other" }},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := context.Background()
			store := newTestStore(t)
			now := time.Date(2026, 7, 23, 7, 0, 0, 0, time.UTC)
			revision := createUADashboardRevision(t, store, now)
			params := uaDashboardPublishParams(revision, "audit-"+strings.ReplaceAll(test.name, " ", "-"), "dashboard-new", now.Add(time.Minute))
			test.mutate(&params)
			published, swapped, err := store.PublishUADashboard(ctx, params)
			require.NoError(t, err)
			require.False(t, swapped)
			require.Empty(t, published.ID)
			current, err := store.GetCurrentSourceRevision(ctx, "ws-1", "kb-1", "src-1")
			require.NoError(t, err)
			require.Empty(t, current.UADashboardCommitSHA)
			events, err := store.ListAuditEvents(ctx, "ws-1", "kb-1", 1, 20)
			require.NoError(t, err)
			require.Empty(t, events)
		})
	}
}

func TestStoreRollsBackUADashboardPointerWhenAuditInsertFails(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Date(2026, 7, 23, 7, 30, 0, 0, time.UTC)
	revision := createUADashboardRevision(t, store, now)
	duplicate := AuditEvent{ID: "audit-duplicate", WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", ActorID: "user-1", Action: "existing", ResourceType: "source", ResourceID: "src-1", Summary: "Existing", Details: `{}`, CreatedAt: now}
	_, err := store.CreateAuditEvent(ctx, duplicate)
	require.NoError(t, err)
	params := uaDashboardPublishParams(revision, duplicate.ID, "dashboard-new", now.Add(time.Minute))

	published, swapped, err := store.PublishUADashboard(ctx, params)
	require.Error(t, err)
	require.False(t, swapped)
	require.Empty(t, published.ID)
	current, loadErr := store.GetCurrentSourceRevision(ctx, "ws-1", "kb-1", "src-1")
	require.NoError(t, loadErr)
	require.Empty(t, current.UADashboardCommitSHA)
}

func TestStoreAllowsOnlyOneConcurrentUADashboardPublication(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	sqlDB, err := store.DB().DB()
	require.NoError(t, err)
	sqlDB.SetMaxOpenConns(1)
	now := time.Date(2026, 7, 23, 8, 0, 0, 0, time.UTC)
	revision := createUADashboardRevision(t, store, now)
	type result struct {
		swapped bool
		err     error
	}
	results := make(chan result, 2)
	start := make(chan struct{})
	var wg sync.WaitGroup
	for i, commit := range []string{"dashboard-a", "dashboard-b"} {
		wg.Add(1)
		go func(index int, commitSHA string) {
			defer wg.Done()
			<-start
			params := uaDashboardPublishParams(revision, "audit-concurrent-"+commitSHA, commitSHA, now.Add(time.Duration(index+1)*time.Minute))
			_, swapped, err := store.PublishUADashboard(ctx, params)
			results <- result{swapped: swapped, err: err}
		}(i, commit)
	}
	close(start)
	wg.Wait()
	close(results)
	winners := 0
	for result := range results {
		require.NoError(t, result.err)
		if result.swapped {
			winners++
		}
	}
	require.Equal(t, 1, winners)
}

func createUADashboardRevision(t *testing.T, store *Store, now time.Time) SourceRevision {
	t.Helper()
	_, err := store.CreateKnowledgeBase(context.Background(), KnowledgeBase{
		ID: "kb-1", WorkspaceID: "ws-1", Slug: "dashboard", Name: "Dashboard",
		RetrievalPolicy: "{}", SourceCollectionState: "{}",
		CreatedBy: "user-1", UpdatedBy: "user-1", CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)
	_, err = store.CreateSource(context.Background(), Source{
		ID: "src-1", WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SourceType: "codebase",
		Status: "ready", Name: "Source", Config: "{}",
		CreatedBy: "user-1", UpdatedBy: "user-1", CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)
	revision, err := store.CreateSourceRevision(context.Background(), SourceRevision{
		ID: "rev-1", WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SourceID: "src-1",
		SourceCommitSHA: "source-sha-1", InsightsCommitSHA: "insights-sha-1", FreshnessStatus: "current",
		IsCurrent: true, CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)
	return revision
}

func uaDashboardPublishParams(revision SourceRevision, auditID, commitSHA string, publishedAt time.Time) PublishUADashboardParams {
	return PublishUADashboardParams{
		WorkspaceID: revision.WorkspaceID, KnowledgeBaseID: revision.KnowledgeBaseID, SourceID: revision.SourceID,
		RevisionID: revision.ID, SourceCommitSHA: revision.SourceCommitSHA, PreviousCommitSHA: revision.UADashboardCommitSHA,
		DashboardCommitSHA: commitSHA, ManifestJSON: `{"files":[]}`, PublishedAt: publishedAt,
		AuditEvent: AuditEvent{ID: auditID, WorkspaceID: revision.WorkspaceID, KnowledgeBaseID: revision.KnowledgeBaseID, ActorID: "user-1", Action: "kb.source.dashboard.import", ResourceType: "source", ResourceID: revision.SourceID, Summary: "Imported dashboard", Details: `{}`, CreatedAt: publishedAt},
	}
}
