package kb

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

	"github.com/go-git/go-git/v5/plumbing"
	"github.com/stretchr/testify/require"
	storekb "github.com/tidbcloud/AgentTeam/internal/store/kb"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

type fakeUADashboardRepository struct {
	publishCalls int
	readCalls    int
	publishErr   error
	commitSHA    string
	content      []byte
}

func (f *fakeUADashboardRepository) PublishUADashboard(_ context.Context, request UADashboardRepositoryPublishRequest) (UADashboardRepositoryPublishResult, error) {
	f.publishCalls++
	digests := make(map[string]string, len(request.Files))
	for path, data := range request.Files {
		digests[path] = plumbing.ComputeHash(plumbing.BlobObject, data).String()
	}
	return UADashboardRepositoryPublishResult{CommitSHA: f.commitSHA, FileDigests: digests}, f.publishErr
}

func (f *fakeUADashboardRepository) ReadUADashboardFile(_ context.Context, request UADashboardRepositoryFileRequest) (UADashboardRepositoryFile, error) {
	f.readCalls++
	return UADashboardRepositoryFile{Content: f.content, BlobSHA: request.ExpectedBlobSHA}, nil
}

func TestUADashboardStatusIsDBOnlyAndRoleAware(t *testing.T) {
	svc, repo := newUADashboardServiceFixture(t)
	svc.uaDashboardRepository = nil
	status, err := svc.GetUADashboardStatus(context.Background(), "ws-1", "viewer", "kb-1", "src-1")
	require.NoError(t, err)
	require.Equal(t, "empty", status.State)
	require.False(t, status.CanImport)
	require.Zero(t, repo.publishCalls+repo.readCalls)

	status, err = svc.GetUADashboardStatus(context.Background(), "ws-1", "owner", "kb-1", "src-1")
	require.NoError(t, err)
	require.True(t, status.CanImport)
	require.Equal(t, []string{"zip", "tar.gz", "tgz"}, status.AcceptedFormats)
}

func TestUADashboardImportRequiresWorkspaceAdministratorAndKBEdit(t *testing.T) {
	svc, repo := newUADashboardServiceFixture(t)
	svc.workspaceGovernanceAuthorizer = fakeWorkspaceGovernanceAuthorizer{authorize: func(_ context.Context, _, principalID string) (bool, error) {
		return principalID == "workspace-admin", nil
	}}
	now := svc.now()
	_, err := svc.store.UpsertKnowledgeBaseGrant(context.Background(), storekb.KnowledgeBaseGrant{
		ID: "grant-admin", WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1",
		SubjectType: "user", SubjectID: "workspace-admin", Role: "edit",
		CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)

	ownerStatus, err := svc.GetUADashboardStatus(context.Background(), "ws-1", "owner", "kb-1", "src-1")
	require.NoError(t, err)
	require.False(t, ownerStatus.CanImport, "a KB owner is not implicitly a workspace administrator")
	_, err = svc.AuthorizeUADashboardImport(context.Background(), "ws-1", "owner", "kb-1", "src-1")
	require.Equal(t, "dashboard-import-forbidden", Code(err))

	adminStatus, err := svc.GetUADashboardStatus(context.Background(), "ws-1", "workspace-admin", "kb-1", "src-1")
	require.NoError(t, err)
	require.True(t, adminStatus.CanImport)
	require.Zero(t, repo.publishCalls+repo.readCalls)
}

func TestUADashboardGovernanceLookupFailurePreservesReadAndFailsImportClosed(t *testing.T) {
	svc, repo := newUADashboardServiceFixture(t)
	authorityErr := errors.New("workspace authority unavailable")
	svc.workspaceGovernanceAuthorizer = fakeWorkspaceGovernanceAuthorizer{authorize: func(context.Context, string, string) (bool, error) {
		return false, authorityErr
	}}

	status, err := svc.GetUADashboardStatus(context.Background(), "ws-1", "owner", "kb-1", "src-1")
	require.NoError(t, err)
	require.False(t, status.CanImport)
	_, err = svc.AuthorizeUADashboardImport(context.Background(), "ws-1", "owner", "kb-1", "src-1")
	require.ErrorIs(t, err, authorityErr)
	require.Zero(t, repo.publishCalls+repo.readCalls)
}

func TestUADashboardUnauthorizedAndCrossScopeNeverCallsRepository(t *testing.T) {
	svc, repo := newUADashboardServiceFixture(t)
	for _, input := range []struct{ workspace, subject, kbID, sourceID string }{
		{"ws-1", "outsider", "kb-1", "src-1"}, {"ws-other", "owner", "kb-1", "src-1"}, {"ws-1", "owner", "kb-1", "src-other"},
	} {
		_, err := svc.GetUADashboardStatus(context.Background(), input.workspace, input.subject, input.kbID, input.sourceID)
		require.Error(t, err)
	}
	require.Zero(t, repo.publishCalls+repo.readCalls)
}

func TestUADashboardImportPublishesCASPointerAndAudit(t *testing.T) {
	svc, repo := newUADashboardServiceFixture(t)
	repo.commitSHA = "dashboard-commit"
	archive := writeUATestArchive(t, "zip", testUAArchiveEntries(t, "", testUAGraph(t)))
	status, err := svc.ImportUADashboard(context.Background(), "ws-1", "owner", "kb-1", "src-1", UADashboardImport{ArchivePath: archive, Filename: "dashboard.zip"})
	require.NoError(t, err)
	require.Equal(t, "ready", status.State)
	require.Equal(t, "dashboard-commit", status.DashboardCommitSHA)
	require.Equal(t, testUASourceSHA, status.DashboardSourceCommitSHA)
	require.False(t, status.UpdateAvailable)
	require.NotEmpty(t, status.Manifest.Files[0].BlobSHA)
	require.Equal(t, 1, repo.publishCalls)
	events, err := svc.store.ListAuditEvents(context.Background(), "ws-1", "kb-1", 1, 10)
	require.NoError(t, err)
	require.Equal(t, "kb.source.dashboard.import", events[0].Action)
	var details map[string]any
	require.NoError(t, json.Unmarshal([]byte(events[0].Details), &details))
	require.Equal(t, "zip", details["archiveFormat"])
	require.Equal(t, "dashboard-commit", details["dashboardCommitSha"])
	require.Equal(t, testUASourceSHA, details["sourceCommitSha"])
	require.EqualValues(t, 5, details["files"])
	require.Greater(t, details["bytes"].(float64), float64(0))
}

func TestUADashboardAGSFailurePreservesCurrentPointer(t *testing.T) {
	svc, repo := newUADashboardServiceFixture(t)
	repo.publishErr = errors.New("AGS unavailable")
	archive := writeUATestArchive(t, "zip", testUAArchiveEntries(t, "", testUAGraph(t)))
	_, err := svc.ImportUADashboard(context.Background(), "ws-1", "owner", "kb-1", "src-1", UADashboardImport{ArchivePath: archive, Filename: "dashboard.zip"})
	require.Error(t, err)
	revision, err := svc.store.GetCurrentSourceRevision(context.Background(), "ws-1", "kb-1", "src-1")
	require.NoError(t, err)
	require.Empty(t, revision.UADashboardCommitSHA)
}

func TestUADashboardFileReadUsesPersistedCurrentManifest(t *testing.T) {
	svc, repo := newUADashboardServiceFixture(t)
	repo.content = []byte(`{"graph":true}`)
	manifest, err := json.Marshal(validTestUADashboardManifest(repo.content))
	require.NoError(t, err)
	now := svc.now()
	_, swapped, err := svc.store.PublishUADashboard(context.Background(), storekb.PublishUADashboardParams{
		WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SourceID: "src-1", RevisionID: "rev-1", SourceCommitSHA: testUASourceSHA,
		DashboardCommitSHA: "dashboard-commit", ManifestJSON: string(manifest), PublishedAt: now,
		AuditEvent: svc.auditEvent("ws-1", "kb-1", "owner", "test", "source", "src-1", "test", ""),
	})
	require.NoError(t, err)
	require.True(t, swapped)
	file, err := svc.ReadUADashboardFile(context.Background(), "ws-1", "viewer", "kb-1", "src-1", "knowledge-graph.json")
	require.NoError(t, err)
	require.Equal(t, repo.content, file.Content)
	require.Equal(t, `"dashboard-commit:blob-sha"`, file.ETag)
	require.Equal(t, 1, repo.readCalls)
}

func TestUADashboardSourceAdvanceKeepsPublicationReadableAndReportsUpdate(t *testing.T) {
	svc, repo := newUADashboardServiceFixture(t)
	repo.content = []byte(`{"graph":true}`)
	persistTestUADashboardManifest(t, svc, validTestUADashboardManifest(repo.content))

	nextSourceSHA := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
	now := svc.now().Add(time.Minute)
	revision, created, err := svc.store.AdvanceSourceRevision(context.Background(), storekb.AdvanceSourceRevisionParams{
		ExpectedCurrentSourceCommitSHA: testUASourceSHA,
		EnforceExpectedCurrent:         true,
		Revision: storekb.SourceRevision{
			ID: "rev-2", WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SourceID: "src-1",
			SourceCommitSHA: nextSourceSHA, IsCurrent: true, CreatedAt: now, UpdatedAt: now,
		},
	})
	require.NoError(t, err)
	require.True(t, created)
	require.Equal(t, "dashboard-commit", revision.UADashboardCommitSHA)

	status, err := svc.GetUADashboardStatus(context.Background(), "ws-1", "viewer", "kb-1", "src-1")
	require.NoError(t, err)
	require.Equal(t, "ready", status.State)
	require.Equal(t, nextSourceSHA, status.SourceCommitSHA)
	require.Equal(t, testUASourceSHA, status.DashboardSourceCommitSHA)
	require.True(t, status.UpdateAvailable)
	require.NotNil(t, status.Manifest)

	file, err := svc.ReadUADashboardFile(context.Background(), "ws-1", "viewer", "kb-1", "src-1", UADashboardKnowledgeGraphFile)
	require.NoError(t, err)
	require.Equal(t, repo.content, file.Content)
	require.Equal(t, 1, repo.readCalls)
}

func validTestUADashboardManifest(content []byte) UADashboardManifest {
	return UADashboardManifest{SchemaVersion: 1, SourceCommitSHA: testUASourceSHA, DashboardCommitSHA: "dashboard-commit", Files: []UADashboardManifestFile{
		{Path: UADashboardConfigFile, Bytes: int64(len(content)), BlobSHA: "config-blob-sha"},
		{Path: UADashboardDomainGraphFile, Bytes: int64(len(content)), BlobSHA: "domain-blob-sha"},
		{Path: UADashboardFingerprintsFile, Bytes: int64(len(content)), BlobSHA: "fingerprints-blob-sha"},
		{Path: UADashboardKnowledgeGraphFile, Bytes: int64(len(content)), BlobSHA: "blob-sha"},
		{Path: UADashboardMetaFile, Bytes: int64(len(content)), BlobSHA: "meta-blob-sha"},
	}}
}

func TestUADashboardHistoricalExactFiveWithRetiredCountsIsReady(t *testing.T) {
	svc, repo := newUADashboardServiceFixture(t)
	manifest := validTestUADashboardManifest([]byte(`{}`))
	encoded, err := json.Marshal(struct {
		UADashboardManifest
		Graph map[string]int `json:"graph"`
	}{UADashboardManifest: manifest, Graph: map[string]int{"nodes": 2580, "edges": 9382, "layers": 9, "tourSteps": 10}})
	require.NoError(t, err)
	now := svc.now()
	_, swapped, err := svc.store.PublishUADashboard(context.Background(), storekb.PublishUADashboardParams{
		WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SourceID: "src-1", RevisionID: "rev-1", SourceCommitSHA: testUASourceSHA,
		DashboardCommitSHA: "dashboard-commit", ManifestJSON: string(encoded), PublishedAt: now,
		AuditEvent: svc.auditEvent("ws-1", "kb-1", "owner", "test", "source", "src-1", "test", ""),
	})
	require.NoError(t, err)
	require.True(t, swapped)
	status, err := svc.GetUADashboardStatus(context.Background(), "ws-1", "viewer", "kb-1", "src-1")
	require.NoError(t, err)
	require.Equal(t, "ready", status.State)
	require.Len(t, status.Manifest.Files, 5)
	require.Zero(t, repo.readCalls+repo.publishCalls)
}

func TestUADashboardLegacyPublicationRequiresReimportWithoutRepositoryRead(t *testing.T) {
	for _, paths := range [][]string{
		{UADashboardKnowledgeGraphFile},
		{UADashboardKnowledgeGraphFile, UADashboardDomainGraphFile},
		{UADashboardKnowledgeGraphFile, UADashboardDomainGraphFile, UADashboardMetaFile, UADashboardConfigFile, UADashboardFingerprintsFile, UADashboardDiffOverlayFile},
	} {
		t.Run(strings.Join(paths, "+"), func(t *testing.T) {
			svc, repo := newUADashboardServiceFixture(t)
			manifest := UADashboardManifest{SchemaVersion: 1, SourceCommitSHA: testUASourceSHA, DashboardCommitSHA: "dashboard-commit"}
			for _, path := range paths {
				manifest.Files = append(manifest.Files, UADashboardManifestFile{Path: path, Bytes: 2, BlobSHA: path + "-blob"})
			}
			persistTestUADashboardManifest(t, svc, manifest)
			status, err := svc.GetUADashboardStatus(context.Background(), "ws-1", "viewer", "kb-1", "src-1")
			require.NoError(t, err)
			require.Equal(t, "reimport_required", status.State)
			require.Equal(t, "dashboard-commit", status.DashboardCommitSHA)
			require.Equal(t, testUASourceSHA, status.SourceCommitSHA)
			require.NotNil(t, status.PublishedAt)
			require.Nil(t, status.Manifest)
			_, err = svc.ReadUADashboardFile(context.Background(), "ws-1", "viewer", "kb-1", "src-1", UADashboardKnowledgeGraphFile)
			require.Equal(t, "dashboard-reimport-required", Code(err))
			require.Zero(t, repo.readCalls)
		})
	}
}

func persistTestUADashboardManifest(t *testing.T, svc *KBService, manifest UADashboardManifest) {
	t.Helper()
	encoded, err := json.Marshal(manifest)
	require.NoError(t, err)
	now := svc.now()
	_, swapped, err := svc.store.PublishUADashboard(context.Background(), storekb.PublishUADashboardParams{
		WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SourceID: "src-1", RevisionID: "rev-1", SourceCommitSHA: testUASourceSHA,
		DashboardCommitSHA: "dashboard-commit", ManifestJSON: string(encoded), PublishedAt: now,
		AuditEvent: svc.auditEvent("ws-1", "kb-1", "owner", "test", "source", "src-1", "test", ""),
	})
	require.NoError(t, err)
	require.True(t, swapped)
}

func TestUADashboardInvalidPublicationFailsClosedWithStableCode(t *testing.T) {
	tests := []struct {
		name     string
		manifest string
	}{
		{name: "malformed", manifest: `{`},
		{name: "schema mismatch", manifest: `{"schemaVersion":2,"sourceCommitSha":"` + testUASourceSHA + `","dashboardCommitSha":"dashboard-commit","files":[{"path":"knowledge-graph.json","bytes":2,"blobSha":"blob-sha"}]}`},
		{name: "missing dashboard source", manifest: `{"schemaVersion":1,"dashboardCommitSha":"dashboard-commit","files":[{"path":"knowledge-graph.json","bytes":2,"blobSha":"blob-sha"}]}`},
		{name: "dashboard mismatch", manifest: `{"schemaVersion":1,"sourceCommitSha":"` + testUASourceSHA + `","dashboardCommitSha":"wrong","files":[{"path":"knowledge-graph.json","bytes":2,"blobSha":"blob-sha"}]}`},
		{name: "duplicate replaces domain", manifest: `{"schemaVersion":1,"sourceCommitSha":"` + testUASourceSHA + `","dashboardCommitSha":"dashboard-commit","files":[{"path":"knowledge-graph.json","bytes":2,"blobSha":"blob-sha"},{"path":"knowledge-graph.json","bytes":2,"blobSha":"blob-sha"}]}`},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			svc, repo := newUADashboardServiceFixture(t)
			now := svc.now()
			_, swapped, err := svc.store.PublishUADashboard(context.Background(), storekb.PublishUADashboardParams{WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SourceID: "src-1", RevisionID: "rev-1", SourceCommitSHA: testUASourceSHA, DashboardCommitSHA: "dashboard-commit", ManifestJSON: test.manifest, PublishedAt: now, AuditEvent: svc.auditEvent("ws-1", "kb-1", "owner", "test", "source", "src-1", "test", "")})
			require.NoError(t, err)
			require.True(t, swapped)
			_, err = svc.GetUADashboardStatus(context.Background(), "ws-1", "owner", "kb-1", "src-1")
			require.Equal(t, "dashboard-publication-invalid", Code(err))
			_, err = svc.ReadUADashboardFile(context.Background(), "ws-1", "owner", "kb-1", "src-1", "knowledge-graph.json")
			require.Equal(t, "dashboard-publication-invalid", Code(err))
			require.Zero(t, repo.readCalls)
		})
	}
}

func TestUADashboardSourcePreviewPrefersAndPinsCurrentRevision(t *testing.T) {
	svc, _ := newUADashboardServiceFixture(t)
	_, err := svc.repositoryStore.Upsert(context.Background(), repos.ResourceRepository{ID: "rr-resolved", WorkspaceID: "ws-1", ResourceType: repos.ResourceTypeKBSource, ResourceID: "src-1", RepoRole: repos.RepoRolePrimary, RepoOwner: "org", RepoName: "resolved", RepoFullName: "org/resolved"})
	require.NoError(t, err)
	preview, err := svc.GetUADashboardSourcePreview(context.Background(), "ws-1", "owner", "kb-1", "src-1")
	require.NoError(t, err)
	require.Equal(t, "org", preview.RepoOwner)
	require.Equal(t, "resolved", preview.RepoName)
	require.Equal(t, testUASourceSHA, preview.SourceCommitSHA)
}

func newUADashboardServiceFixture(t *testing.T) (*KBService, *fakeUADashboardRepository) {
	t.Helper()
	svc := newKBServiceForTest(t)
	repo := &fakeUADashboardRepository{}
	svc.uaDashboardRepository = repo
	now := svc.now()
	_, err := svc.store.CreateKnowledgeBase(context.Background(), storekb.KnowledgeBase{ID: "kb-1", WorkspaceID: "ws-1", Name: "KB", IsActive: true, DefaultWorkspaceMemberRole: "none", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.store.UpsertKnowledgeBaseGrant(context.Background(), storekb.KnowledgeBaseGrant{ID: "grant-view", WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SubjectType: "user", SubjectID: "viewer", Role: "view", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.store.UpsertKnowledgeBaseGrant(context.Background(), storekb.KnowledgeBaseGrant{ID: "grant-deny", WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SubjectType: "user", SubjectID: "outsider", Role: "none", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.store.CreateSource(context.Background(), storekb.Source{ID: "src-1", WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SourceType: "codebase", Status: "ready", Name: "AgentTeam", Config: "{}", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.store.CreateSourceRevision(context.Background(), storekb.SourceRevision{ID: "rev-1", WorkspaceID: "ws-1", KnowledgeBaseID: "kb-1", SourceID: "src-1", SourceCommitSHA: testUASourceSHA, IsCurrent: true, CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.repositoryStore.Upsert(context.Background(), repos.ResourceRepository{ID: "rr-1", WorkspaceID: "ws-1", ResourceType: repos.ResourceTypeKBSource, ResourceID: "src-1", RepoRole: repos.RepoRoleInsights, RepoOwner: "org", RepoName: "repo", RepoFullName: "org/repo", DefaultRef: "main", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	return svc, repo
}

var _ = time.Time{}
