package kb

import (
	"context"
	"strings"
	"testing"
	"time"

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

func seedWikiGenerationSource(t *testing.T, store *Store, now time.Time) {
	t.Helper()
	ctx := context.Background()
	_, err := store.CreateKnowledgeBase(ctx, KnowledgeBase{
		ID: "kb_1", WorkspaceID: "ws_1", GroupID: "group_1", Name: "KB",
		PublicationState: "draft", IsActive: true, RetrievalPolicy: "{}",
		SourceCollectionState: "{}", CreatedBy: "owner", UpdatedBy: "owner",
		CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)
	_, err = store.CreateSource(ctx, Source{
		ID: "src_1", WorkspaceID: "ws_1", KnowledgeBaseID: "kb_1",
		Name: "Source", SourceType: "file_repository", Status: "ready",
		SyncStatus: "ready", IndexStatus: "ready", Config: "{}",
		CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)
	_, err = store.CreateSourceRevision(ctx, SourceRevision{
		ID: "rev_1", WorkspaceID: "ws_1", KnowledgeBaseID: "kb_1",
		SourceID: "src_1", SourceCommitSHA: "source-sha-1", IsCurrent: true,
		FreshnessStatus: "current", CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)
}

func directWikiGenerationFixture(now time.Time) (KnowledgeBaseGenerationRequest, Job) {
	request := KnowledgeBaseGenerationRequest{
		ID: "kbgr_1", WorkspaceID: "ws_1", KnowledgeBaseID: "kb_1",
		SourceID: "src_1", Kind: GenerationKindWiki,
		SourceRevisionID: "rev_1", SourceCommitSHA: "source-sha-1",
		Scope: "full", Language: "auto", Reason: "Document this source",
		RequesterID: "owner", Status: GenerationRequestStatusQueued,
		JobID: "job_1", IdempotencyKey: "request-1", PayloadDigest: strings.Repeat("a", 64),
		Version: 1, CreatedAt: now, UpdatedAt: now,
	}
	job := Job{
		ID: request.JobID, WorkspaceID: request.WorkspaceID,
		KnowledgeBaseID: request.KnowledgeBaseID, SourceID: request.SourceID,
		JobType: "generate_source_wiki", Status: "queued", Progress: "{}",
		CreatedAt: now, UpdatedAt: now,
	}
	return request, job
}

func pendingWikiGenerationFixture(now time.Time) KnowledgeBaseGenerationRequest {
	request, _ := directWikiGenerationFixture(now)
	request.ID = "kbgr_pending"
	request.Status = GenerationRequestStatusPendingApproval
	request.JobID = ""
	request.ApprovalID = "appr_1"
	request.ApprovalVersion = 1
	request.IdempotencyKey = "pending-request-1"
	return request
}

func TestCreateDirectWikiGenerationIsRevisionFencedAndIdempotent(t *testing.T) {
	store := newTestStore(t)
	now := time.Date(2026, 8, 9, 2, 3, 4, 0, time.UTC)
	seedWikiGenerationSource(t, store, now)
	request, job := directWikiGenerationFixture(now)

	created, inserted, err := store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: request, Job: &job, Now: now})
	require.NoError(t, err)
	require.True(t, inserted)
	require.Equal(t, request.ID, created.ID)

	replayed, inserted, err := store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: request, Job: &job, Now: now})
	require.NoError(t, err)
	require.False(t, inserted)
	require.Equal(t, created.ID, replayed.ID)

	conflicting := request
	conflicting.Reason = "different"
	_, _, err = store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: conflicting, Job: &job, Now: now})
	require.ErrorIs(t, err, ErrGenerationRequestReplayConflict)

	second, secondJob := request, job
	second.ID, second.JobID, second.IdempotencyKey = "kbgr_2", "job_2", "request-2"
	secondJob.ID = second.JobID
	_, _, err = store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: second, Job: &secondJob, Now: now})
	require.ErrorIs(t, err, ErrGenerationRequestAlreadyActive)

	requests, err := store.ListLatestGenerationRequestsBySource(t.Context(), "ws_1", "kb_1", GenerationKindWiki, []string{"src_1"})
	require.NoError(t, err)
	require.Equal(t, []KnowledgeBaseGenerationRequest{created}, requests)
	jobs, err := store.ListSourceJobs(t.Context(), "ws_1", "kb_1", "src_1", "generate_source_wiki", 1, 10)
	require.NoError(t, err)
	require.Len(t, jobs, 1)
	byJob, found, err := store.FindGenerationRequestByJob(t.Context(), "ws_1", job.ID)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, request.ID, byJob.ID)
}

func TestCreateDirectWikiGenerationRejectsChangedRevisionWithoutRows(t *testing.T) {
	store := newTestStore(t)
	now := time.Date(2026, 8, 9, 2, 3, 4, 0, time.UTC)
	seedWikiGenerationSource(t, store, now)
	request, job := directWikiGenerationFixture(now)
	request.SourceRevisionID = "rev_stale"

	_, _, err := store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: request, Job: &job, Now: now})
	require.ErrorIs(t, err, ErrGenerationSourceRevisionChanged)
	_, err = store.GetGenerationRequest(t.Context(), "ws_1", request.ID)
	require.Error(t, err)
	_, err = store.GetJob(t.Context(), "ws_1", job.ID)
	require.Error(t, err)
}

func TestCreateDirectWikiGenerationRejectsAlreadyPublishedRevision(t *testing.T) {
	store := newTestStore(t)
	now := time.Date(2026, 8, 9, 2, 3, 4, 0, time.UTC)
	seedWikiGenerationSource(t, store, now)
	revision, err := store.GetCurrentSourceRevision(t.Context(), "ws_1", "kb_1", "src_1")
	require.NoError(t, err)
	revision.PublishedWikiRevision = "wiki-ready"
	revision.UpdatedAt = now.Add(time.Minute)
	require.NoError(t, store.DB().Save(&revision).Error)
	request, job := directWikiGenerationFixture(now)

	_, _, err = store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: request, Job: &job, Now: now})
	require.ErrorIs(t, err, ErrGenerationArtifactAlreadyReady)
}

func TestResolveWikiGenerationApprovalQueuesJobAndReplaysDecision(t *testing.T) {
	store := newTestStore(t)
	now := time.Date(2026, 8, 9, 2, 3, 4, 0, time.UTC)
	seedWikiGenerationSource(t, store, now)
	request := pendingWikiGenerationFixture(now)
	created, inserted, err := store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: request, Now: now})
	require.NoError(t, err)
	require.True(t, inserted)
	require.Equal(t, GenerationRequestStatusPendingApproval, created.Status)

	job := Job{
		ID: "job_approved", WorkspaceID: "ws_1", KnowledgeBaseID: "kb_1",
		SourceID: "src_1", JobType: "generate_source_wiki", Status: "queued",
		Progress: `{}`, CreatedAt: now.Add(time.Minute), UpdatedAt: now.Add(time.Minute),
	}
	params := ResolveWikiGenerationApprovalParams{
		WorkspaceID: "ws_1", ApprovalID: request.ApprovalID, ApprovalVersion: 2, ActorID: "admin",
		ClientDecisionID: "decision-1", Decision: GenerationDecisionApproved,
		Job: &job, AuditEvent: AuditEvent{
			ID: "audit_approval", WorkspaceID: "ws_1", KnowledgeBaseID: "kb_1",
			ActorID: "admin", Action: "kb.wiki.approval.resolved", ResourceType: "source",
			ResourceID: "src_1", Summary: "Approved", Details: `{}`, CreatedAt: now.Add(time.Minute),
		}, Now: now.Add(time.Minute),
	}
	resolved, applied, err := store.ResolveWikiGenerationApproval(t.Context(), params)
	require.NoError(t, err)
	require.True(t, applied)
	require.Equal(t, GenerationRequestStatusQueued, resolved.Status)
	require.Equal(t, job.ID, resolved.JobID)
	require.Equal(t, GenerationDecisionApproved, resolved.Decision)
	require.Equal(t, int64(2), resolved.ApprovalVersion)

	replayed, applied, err := store.ResolveWikiGenerationApproval(t.Context(), params)
	require.NoError(t, err)
	require.False(t, applied)
	require.Equal(t, resolved.ID, replayed.ID)
	wrongVersion := params
	wrongVersion.ApprovalVersion = 3
	_, _, err = store.ResolveWikiGenerationApproval(t.Context(), wrongVersion)
	require.ErrorIs(t, err, ErrGenerationRequestReplayConflict)
	jobs, err := store.ListSourceJobs(t.Context(), "ws_1", "kb_1", "src_1", "generate_source_wiki", 1, 10)
	require.NoError(t, err)
	require.Len(t, jobs, 1)
	audits, err := store.ListAuditEvents(t.Context(), "ws_1", "kb_1", 1, 10)
	require.NoError(t, err)
	require.Len(t, audits, 1)
}

func TestResolveWikiGenerationApprovalMakesChangedSourceStaleWithoutJob(t *testing.T) {
	store := newTestStore(t)
	now := time.Date(2026, 8, 9, 2, 3, 4, 0, time.UTC)
	seedWikiGenerationSource(t, store, now)
	request := pendingWikiGenerationFixture(now)
	_, _, err := store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: request, Now: now})
	require.NoError(t, err)
	_, _, err = store.AdvanceSourceRevision(t.Context(), 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",
			FreshnessStatus: "current", CreatedAt: now.Add(time.Minute), UpdatedAt: now.Add(time.Minute),
		},
	})
	require.NoError(t, err)
	job := Job{ID: "job_stale", WorkspaceID: "ws_1", KnowledgeBaseID: "kb_1", SourceID: "src_1", JobType: "generate_source_wiki", Status: "queued", CreatedAt: now, UpdatedAt: now}

	resolved, applied, err := store.ResolveWikiGenerationApproval(t.Context(), ResolveWikiGenerationApprovalParams{
		WorkspaceID: "ws_1", ApprovalID: request.ApprovalID, ApprovalVersion: 2, ActorID: "admin",
		ClientDecisionID: "decision-stale", Decision: GenerationDecisionApproved,
		Job: &job, Now: now.Add(2 * time.Minute),
	})
	require.NoError(t, err)
	require.True(t, applied)
	require.Equal(t, GenerationRequestStatusStale, resolved.Status)
	require.Equal(t, GenerationDecisionApproved, resolved.Decision)
	require.Equal(t, "source_revision_changed", resolved.DecisionReason)
	_, err = store.GetJob(t.Context(), "ws_1", job.ID)
	require.Error(t, err)
	replayed, applied, err := store.ResolveWikiGenerationApproval(t.Context(), ResolveWikiGenerationApprovalParams{
		WorkspaceID: "ws_1", ApprovalID: request.ApprovalID, ApprovalVersion: 2, ActorID: "admin",
		ClientDecisionID: "decision-stale", Decision: GenerationDecisionApproved,
		Job: &Job{
			ID: "job_retry", WorkspaceID: "ws_1", KnowledgeBaseID: "kb_1",
			SourceID: "src_1", JobType: "generate_source_wiki", Status: "queued",
		},
		Now: now.Add(3 * time.Minute),
	})
	require.NoError(t, err)
	require.False(t, applied)
	require.Equal(t, GenerationRequestStatusStale, replayed.Status)
}

func TestResolveWikiGenerationApprovalMakesDeletedSourceStaleWithoutJob(t *testing.T) {
	store := newTestStore(t)
	now := time.Date(2026, 8, 9, 2, 3, 4, 0, time.UTC)
	seedWikiGenerationSource(t, store, now)
	request := pendingWikiGenerationFixture(now)
	_, _, err := store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: request, Now: now})
	require.NoError(t, err)
	require.NoError(t, store.DeleteSource(t.Context(), "ws_1", "kb_1", "src_1"))
	job := Job{
		ID: "job_deleted", WorkspaceID: "ws_1", KnowledgeBaseID: "kb_1",
		SourceID: "src_1", JobType: "generate_source_wiki", Status: "queued",
	}

	resolved, applied, err := store.ResolveWikiGenerationApproval(t.Context(), ResolveWikiGenerationApprovalParams{
		WorkspaceID: "ws_1", ApprovalID: request.ApprovalID, ApprovalVersion: 2, ActorID: "admin",
		ClientDecisionID: "decision-deleted", Decision: GenerationDecisionApproved,
		Job: &job, Now: now.Add(time.Minute),
	})
	require.NoError(t, err)
	require.True(t, applied)
	require.Equal(t, GenerationRequestStatusStale, resolved.Status)
	_, err = store.GetJob(t.Context(), "ws_1", job.ID)
	require.Error(t, err)
}

func TestResolveWikiGenerationApprovalRejectsAndWithdraws(t *testing.T) {
	for _, test := range []struct {
		name, decision, reason, wantStatus string
	}{
		{name: "rejected", decision: GenerationDecisionRejected, reason: "Needs a narrower explanation", wantStatus: GenerationRequestStatusRejected},
		{name: "withdrawn", decision: GenerationDecisionWithdrawn, wantStatus: GenerationRequestStatusWithdrawn},
	} {
		t.Run(test.name, func(t *testing.T) {
			store := newTestStore(t)
			now := time.Date(2026, 8, 9, 2, 3, 4, 0, time.UTC)
			seedWikiGenerationSource(t, store, now)
			request := pendingWikiGenerationFixture(now)
			_, _, err := store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: request, Now: now})
			require.NoError(t, err)
			resolved, applied, err := store.ResolveWikiGenerationApproval(t.Context(), ResolveWikiGenerationApprovalParams{
				WorkspaceID: "ws_1", ApprovalID: request.ApprovalID, ApprovalVersion: 2, ActorID: "member",
				ClientDecisionID: "decision-1", Decision: test.decision,
				Reason: test.reason, Now: now.Add(time.Minute),
			})
			require.NoError(t, err)
			require.True(t, applied)
			require.Equal(t, test.wantStatus, resolved.Status)
			require.Equal(t, test.reason, resolved.DecisionReason)
		})
	}
}

func TestFinishWikiGenerationPublishesPointerRequestJobAndAuditAtomically(t *testing.T) {
	store := newTestStore(t)
	now := time.Date(2026, 8, 9, 2, 3, 4, 0, time.UTC)
	seedWikiGenerationSource(t, store, now)
	request, job := directWikiGenerationFixture(now)
	_, _, err := store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: request, Job: &job, Now: now})
	require.NoError(t, err)
	claimed, ok, err := store.ClaimNextQueuedJob(t.Context(), job.JobType, "worker-1", time.Hour, now.Add(time.Minute))
	require.NoError(t, err)
	require.True(t, ok)
	_, ok, err = store.MarkWikiGenerationRunning(t.Context(), "ws_1", request.ID, job.ID, now.Add(time.Minute))
	require.NoError(t, err)
	require.True(t, ok)

	finished, err := store.FinishWikiGeneration(t.Context(), FinishWikiGenerationParams{
		WorkspaceID: "ws_1", RequestID: request.ID, JobID: job.ID,
		LeaseOwner: "worker-1", AttemptCount: claimed.AttemptCount,
		InsightsCommitSHA: "insights-sha", PublishedWikiCommit: "wiki-sha",
		AuditEvent: AuditEvent{
			ID: "audit_1", WorkspaceID: "ws_1", KnowledgeBaseID: "kb_1",
			ActorID: "owner", Action: "kb.wiki.generated", ResourceType: "source",
			ResourceID: "src_1", Summary: "Published", Details: "{}", CreatedAt: now.Add(2 * time.Minute),
		},
		Now: now.Add(2 * time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, GenerationRequestStatusReady, finished.Status)
	require.Equal(t, "wiki-sha", finished.ArtifactRevision)
	revision, err := store.GetCurrentSourceRevision(t.Context(), "ws_1", "kb_1", "src_1")
	require.NoError(t, err)
	require.Equal(t, "insights-sha", revision.InsightsCommitSHA)
	require.Equal(t, "wiki-sha", revision.PublishedWikiRevision)
	storedJob, err := store.GetJob(t.Context(), "ws_1", job.ID)
	require.NoError(t, err)
	require.Equal(t, "succeeded", storedJob.Status)
	audits, err := store.ListAuditEvents(t.Context(), "ws_1", "kb_1", 1, 10)
	require.NoError(t, err)
	require.Len(t, audits, 1)
}

func TestWikiGenerationTerminalNotificationProjectionIsVersionFenced(t *testing.T) {
	now := time.Date(2026, 8, 9, 12, 0, 0, 0, time.UTC)
	store := newTestStore(t)
	seedWikiGenerationSource(t, store, now)
	request, job := directWikiGenerationFixture(now)
	_, _, err := store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: request, Job: &job, Now: now})
	require.NoError(t, err)
	claimed, err := store.ClaimQueuedJobsFair(t.Context(), "generate_source_wiki", "worker-1", time.Hour, 1, now.Add(time.Minute), []string{"ws_1"})
	require.NoError(t, err)
	require.Len(t, claimed, 1)
	_, running, err := store.MarkWikiGenerationRunning(t.Context(), "ws_1", request.ID, job.ID, now.Add(time.Minute))
	require.NoError(t, err)
	require.True(t, running)
	finished, err := store.FinishWikiGeneration(t.Context(), FinishWikiGenerationParams{
		WorkspaceID: "ws_1", RequestID: request.ID, JobID: job.ID,
		LeaseOwner: "worker-1", AttemptCount: claimed[0].AttemptCount,
		PublishedWikiCommit: "wiki-terminal", Now: now.Add(2 * time.Minute),
		AuditEvent: AuditEvent{
			ID: "audit_terminal", WorkspaceID: "ws_1", KnowledgeBaseID: "kb_1",
			ActorID: "owner", Action: "kb.wiki.generated", ResourceType: "source",
			ResourceID: "src_1", Summary: "Published", Details: "{}", CreatedAt: now.Add(2 * time.Minute),
		},
	})
	require.NoError(t, err)
	require.Equal(t, GenerationRequestStatusReady, finished.Status)
	require.Zero(t, finished.NotificationVersion)

	foreignOnly, err := store.ListPendingWikiGenerationTerminalNotifications(t.Context(), 10, []string{})
	require.NoError(t, err)
	require.Empty(t, foreignOnly)
	pending, err := store.ListPendingWikiGenerationTerminalNotifications(t.Context(), 10, []string{"ws_1"})
	require.NoError(t, err)
	require.Len(t, pending, 1)
	require.Equal(t, request.ID, pending[0].ID)

	marked, err := store.MarkWikiGenerationTerminalNotified(t.Context(), "ws_1", request.ID, finished.Version+1, now.Add(3*time.Minute))
	require.NoError(t, err)
	require.False(t, marked)
	marked, err = store.MarkWikiGenerationTerminalNotified(t.Context(), "ws_1", request.ID, finished.Version, now.Add(3*time.Minute))
	require.NoError(t, err)
	require.True(t, marked)
	marked, err = store.MarkWikiGenerationTerminalNotified(t.Context(), "ws_1", request.ID, finished.Version, now.Add(3*time.Minute))
	require.NoError(t, err)
	require.False(t, marked)

	stored, err := store.GetGenerationRequest(t.Context(), "ws_1", request.ID)
	require.NoError(t, err)
	require.Equal(t, finished.Version, stored.Version)
	require.Equal(t, finished.Version, stored.NotificationVersion)
	require.NotNil(t, stored.NotifiedAt)
	pending, err = store.ListPendingWikiGenerationTerminalNotifications(t.Context(), 10, []string{"ws_1"})
	require.NoError(t, err)
	require.Empty(t, pending)
}

func TestFinishWikiGenerationMakesChangedRevisionStale(t *testing.T) {
	store := newTestStore(t)
	now := time.Date(2026, 8, 9, 2, 3, 4, 0, time.UTC)
	seedWikiGenerationSource(t, store, now)
	request, job := directWikiGenerationFixture(now)
	_, _, err := store.CreateWikiGeneration(t.Context(), CreateWikiGenerationParams{Request: request, Job: &job, Now: now})
	require.NoError(t, err)
	claimed, ok, err := store.ClaimNextQueuedJob(t.Context(), job.JobType, "worker-1", time.Hour, now.Add(time.Minute))
	require.NoError(t, err)
	require.True(t, ok)
	_, ok, err = store.MarkWikiGenerationRunning(t.Context(), "ws_1", request.ID, job.ID, now.Add(time.Minute))
	require.NoError(t, err)
	require.True(t, ok)
	_, _, err = store.AdvanceSourceRevision(t.Context(), 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",
			FreshnessStatus: "current", CreatedAt: now.Add(2 * time.Minute), UpdatedAt: now.Add(2 * time.Minute),
		},
	})
	require.NoError(t, err)

	finished, err := store.FinishWikiGeneration(t.Context(), FinishWikiGenerationParams{
		WorkspaceID: "ws_1", RequestID: request.ID, JobID: job.ID,
		LeaseOwner: "worker-1", AttemptCount: claimed.AttemptCount,
		InsightsCommitSHA: "orphan-insights", PublishedWikiCommit: "orphan-wiki",
		Now: now.Add(3 * time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, GenerationRequestStatusStale, finished.Status)
	revision, err := store.GetCurrentSourceRevision(t.Context(), "ws_1", "kb_1", "src_1")
	require.NoError(t, err)
	require.Empty(t, revision.PublishedWikiRevision)
	storedJob, err := store.GetJob(t.Context(), "ws_1", job.ID)
	require.NoError(t, err)
	require.Equal(t, "cancelled", storedJob.Status)
	audits, err := store.ListAuditEvents(t.Context(), "ws_1", "kb_1", 1, 10)
	require.NoError(t, err)
	require.Empty(t, audits)
}
