package skills

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	git "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/filemode"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	contentservice "github.com/tidbcloud/AgentTeam/internal/service/content"
	"github.com/tidbcloud/AgentTeam/internal/service/content/migration"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	contentstore "github.com/tidbcloud/AgentTeam/internal/store/content"
	migrationstore "github.com/tidbcloud/AgentTeam/internal/store/content/migration"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/activity"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/memory"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	store "github.com/tidbcloud/AgentTeam/internal/store/resource/skills"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type fakeSkillMigrationRepositoryContent struct {
	*fakeRepositoryContentManager
	exports atomic.Int64
}

func (fake *fakeSkillMigrationRepositoryContent) ExportExactSkillRepository(_ context.Context, _ repos.RepositoryIdentity, _ string, _ contentpayload.Limits) (migration.Export, error) {
	fake.exports.Add(1)
	return migration.Export{Files: []migration.File{
		{Path: "README.md", Data: []byte("skills")},
		{Path: "skills/reviewer/SKILL.md", Data: []byte("---\nname: Reviewer\ndescription: Review production changes\n---\n")},
	}}, nil
}

func TestExactSkillContentMigrationResumesProjectionAndDoesNotCutAuthority(t *testing.T) {
	ctx := context.Background()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&store.Skill{}, &store.SkillVersion{}, &store.SkillRepositoryBranch{}, &store.SkillAgentBinding{}, &store.SkillGrant{},
		&reposstore.ResourceRepository{}, &reposstore.ResourceRepositoryAccessGrant{}, &reposstore.ResourceRepositoryLifecycleEvent{},
		&memory.AgentAssetAccessChangeOutbox{}, &activity.ResourceActivityEvent{}, &activity.ResourceActivityLatestCommit{},
		&contentstore.Authority{}, &contentstore.VersionedContent{},
		&contentstore.ContentVersion{}, &contentstore.VersionOperation{}, &contentstore.VersionPath{},
		&contentstore.VersionPin{}, &contentstore.Outbox{}, &contentstore.StagedPayloadGC{},
		&migrationstore.Mapping{}, &migrationstore.Job{},
	))
	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	var ids atomic.Int64
	newID := func(prefix string) (string, error) { return fmt.Sprintf("%s_%d", prefix, ids.Add(1)), nil }
	core, err := contentservice.NewTree(contentstore.New(db), tree, newID)
	require.NoError(t, err)
	migration, err := migration.NewWorker(migrationstore.New(db), core, newID, skillContentDomain, "skill-migration-test", 3*time.Minute)
	require.NoError(t, err)
	authority := contentstore.New(db)
	now := time.Date(2026, 8, 21, 4, 0, 0, 0, time.UTC)
	require.NoError(t, db.Create(&contentstore.Authority{
		ID: "cta_skill_1", WorkspaceID: "wsp_1", Domain: skillContentDomain, BindingKey: "skill_1",
		State: contentstore.AuthorityStateLegacyActive, Generation: 1, CreatedAt: now, UpdatedAt: now, UpdatedBy: "operator",
	}).Error)
	require.NoError(t, db.Create(&store.Skill{
		ID: "skill_1", WorkspaceID: "wsp_1", Slug: "reviewer", Name: "Reviewer", Description: "Review",
		StorageScope: "personal", StorageOwnerLogin: "usr_1", VisibilityScope: "private", VisibilityOwnerID: "usr_1",
		IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now,
	}).Error)
	require.NoError(t, db.Create(&store.SkillGrant{
		ID: "skg_1", WorkspaceID: "wsp_1", SkillID: "skill_1", SubjectType: "user", SubjectID: "usr_1", Role: "owner",
		CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now,
	}).Error)
	require.NoError(t, db.Create(&reposstore.ResourceRepository{
		ID: "rrp_1", WorkspaceID: "wsp_1", ResourceType: reposstore.ResourceTypeSkillRepository,
		ResourceID: "skill_1", RepoRole: reposstore.RepoRolePrimary, OwnerKind: "user",
		RepoOwner: "usr_1", RepoName: "reviewer.sk", RepoFullName: "usr_1/reviewer.sk", AGSRepoID: "42", DefaultRef: "main",
		Visibility: "private", CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now,
	}).Error)
	repositoryContent := &fakeSkillMigrationRepositoryContent{fakeRepositoryContentManager: &fakeRepositoryContentManager{}}
	service := NewWithOptions(store.New(db), reposstore.New(db), &fakeRepositoryEnsurer{}, func() time.Time { return now }, ServiceOptions{
		RepositoryContentManager: repositoryContent, ActivityStore: activity.New(db), ContentAuthority: authority,
		NativeContent:        NativeSkillContentStore{Content: core},
		NativeContentEnabled: true, MigrationWorker: migration,
	})

	var failProjection atomic.Bool
	failProjection.Store(true)
	require.NoError(t, db.Callback().Create().Before("gorm:create").Register("test:skill_migration_projection_failure", func(tx *gorm.DB) {
		if tx.Statement != nil && tx.Statement.Schema != nil && tx.Statement.Schema.Table == (store.SkillVersion{}).TableName() && failProjection.CompareAndSwap(true, false) {
			tx.AddError(errors.New("injected skill migration projection failure"))
		}
	}))
	request := SkillContentMigrationRequest{RepositoryID: "42", CommitSHA: strings.Repeat("a", 40)}
	_, err = service.RunExactSkillContentMigration(ctx, "wsp_1", "usr_1", "skill_1", request)
	require.ErrorContains(t, err, "injected skill migration projection failure")
	require.EqualValues(t, 1, repositoryContent.exports.Load())

	result, err := service.RunExactSkillContentMigration(ctx, "wsp_1", "usr_1", "skill_1", request)
	require.NoError(t, err)
	require.EqualValues(t, 1, repositoryContent.exports.Load(), "mapped retry must not clone AGS again")
	require.NotEmpty(t, result.ContentID)
	require.NotEmpty(t, result.ContentVersionID)
	require.Equal(t, 1, result.SkillCount)

	migrated, err := store.New(db).GetSkill(ctx, "wsp_1", "skill_1")
	require.NoError(t, err)
	require.Equal(t, result.ContentID, migrated.ContentID)
	require.Equal(t, result.ContentVersionID, migrated.ActiveContentVersionID)
	projected, found, err := store.New(db).FindSkillVersionByContentVersion(ctx, "wsp_1", "skill_1", result.ContentID, result.ContentVersionID)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, result.RootDigest, projected.RootDigest)

	exact, found, err := authority.GetAuthority(ctx, "wsp_1", skillContentDomain, "skill_1")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, contentstore.AuthorityStateLegacyActive, exact.State, "verified mapping must not cut authority")
	var migrationActivity int64
	require.NoError(t, db.Model(&activity.ResourceActivityEvent{}).Where("asset_id = ? AND repo_full_name LIKE ?", "skill_1", "contentstore://%").Count(&migrationActivity).Error)
	require.Zero(t, migrationActivity, "mapping projection must not create a user-visible activity event")

	replayed, err := service.RunExactSkillContentMigration(ctx, "wsp_1", "usr_1", "skill_1", request)
	require.NoError(t, err)
	require.True(t, replayed.Replayed)
	require.Equal(t, result.ContentVersionID, replayed.ContentVersionID)
	require.EqualValues(t, 1, repositoryContent.exports.Load())
}

func TestExportExactSkillRepositoryReadsOnlyVerifiedSnapshot(t *testing.T) {
	manager, repository, commit, _ := newExactSkillMigrationRepositoryForTest(t, map[string]string{
		"README.md":                "private notes",
		"skills/reviewer/SKILL.md": "---\nname: Reviewer\ndescription: Review changes\n---\n",
	}, 0)
	limits := contentpayload.Limits{MaxFiles: 10, MaxFileBytes: 1024, MaxTotalBytes: 4096, MaxManifestBytes: 4096}

	exported, err := manager.ExportExactSkillRepository(context.Background(), repository, commit, limits)
	require.NoError(t, err)
	require.Equal(t, []string{"README.md", "skills/reviewer/SKILL.md"}, []string{exported.Files[0].Path, exported.Files[1].Path})
	require.Equal(t, "private notes", string(exported.Files[0].Data))

	_, err = manager.ExportExactSkillRepository(context.Background(), repository, strings.Repeat("0", 40), limits)
	require.ErrorIs(t, err, ErrSkillRepositorySnapshotChanged)
}

func TestExportExactSkillRepositoryEnforcesFileAndByteBounds(t *testing.T) {
	manager, repository, commit, _ := newExactSkillMigrationRepositoryForTest(t, map[string]string{"one.txt": "1234", "two.txt": "5678"}, 0)

	_, err := manager.ExportExactSkillRepository(context.Background(), repository, commit, contentpayload.Limits{
		MaxFiles: 1, MaxFileBytes: 8, MaxTotalBytes: 16, MaxManifestBytes: 1024,
	})
	require.ErrorIs(t, err, ErrInvalidArgument)

	_, err = manager.ExportExactSkillRepository(context.Background(), repository, commit, contentpayload.Limits{
		MaxFiles: 2, MaxFileBytes: 3, MaxTotalBytes: 16, MaxManifestBytes: 1024,
	})
	require.ErrorIs(t, err, ErrInvalidArgument)

	_, err = manager.ExportExactSkillRepository(context.Background(), repository, commit, contentpayload.Limits{
		MaxFiles: 2, MaxFileBytes: 8, MaxTotalBytes: 7, MaxManifestBytes: 1024,
	})
	require.ErrorIs(t, err, ErrInvalidArgument)
}

func TestSkillMigrationCloneIsShallowAndSingleBranch(t *testing.T) {
	options := skillMigrationCloneOptions("https://ags.test/usr/reviewer.sk.git", " main ")
	require.Equal(t, 1, options.Depth)
	require.True(t, options.SingleBranch)
	require.Equal(t, git.NoTags, options.Tags)
	require.Equal(t, plumbing.NewBranchReferenceName("main"), options.ReferenceName)
}

func TestSkillMigrationCloneDoesNotFetchOldTaggedObjects(t *testing.T) {
	manager, repository, _, source := newExactSkillMigrationRepositoryForTest(t, map[string]string{
		"README.md": strings.Repeat("old source payload", 64*1024),
	}, 1)
	head, err := source.Head()
	require.NoError(t, err)
	headCommit, err := source.CommitObject(head.Hash())
	require.NoError(t, err)
	oldCommit, err := headCommit.Parent(0)
	require.NoError(t, err)
	oldTree, err := oldCommit.Tree()
	require.NoError(t, err)
	oldReadme, err := oldTree.File("README.md")
	require.NoError(t, err)
	require.NoError(t, source.Storer.SetReference(plumbing.NewHashReference(
		plumbing.NewTagReferenceName("old-tag"), oldCommit.Hash,
	)))

	cloned, err := manager.cloneExactSkillMigrationRepository(context.Background(), repository, filepath.Join(t.TempDir(), "clone.git"))
	require.NoError(t, err)
	_, err = cloned.Reference(plumbing.NewTagReferenceName("old-tag"), true)
	require.ErrorIs(t, err, plumbing.ErrReferenceNotFound)
	_, err = cloned.CommitObject(oldCommit.Hash)
	require.ErrorIs(t, err, plumbing.ErrObjectNotFound)
	_, err = cloned.BlobObject(oldReadme.Hash)
	require.ErrorIs(t, err, plumbing.ErrObjectNotFound)
}

func TestExportExactSkillRepositoryUsesShallowCloneForDeepHistory(t *testing.T) {
	manager, repository, commit, _ := newExactSkillMigrationRepositoryForTest(t, map[string]string{"README.md": "seed"}, 32)
	exported, err := manager.ExportExactSkillRepository(context.Background(), repository, commit, contentpayload.Limits{
		MaxFiles: 2, MaxFileBytes: 1024, MaxTotalBytes: 2048, MaxManifestBytes: 2048,
	})
	require.NoError(t, err)
	require.Len(t, exported.Files, 1)
	require.Equal(t, "history-31", string(exported.Files[0].Data))
}

func TestExportExactSkillRepositoryRejectsLiveRepositoryIdentityMismatchBeforeClone(t *testing.T) {
	manager := GoGitRepositoryContentManager{
		Handler: http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
			_ = json.NewEncoder(response).Encode(map[string]any{"id": 43})
		}),
		BaseURL: "unsupported://ags.test",
	}
	_, err := manager.ExportExactSkillRepository(context.Background(), repos.RepositoryIdentity{
		ID: "42", FullName: "usr_owner/reviewer.sk", DefaultBranch: "main",
	}, strings.Repeat("a", 40), contentpayload.Limits{MaxFiles: 2, MaxFileBytes: 1024, MaxTotalBytes: 2048, MaxManifestBytes: 2048})
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.ErrorContains(t, err, "live repository identity")
}

func TestExportExactSkillRepositoryRejectsGitlinkFromHeadTree(t *testing.T) {
	manager, repository, commit := newSkillMigrationRepositoryWithGitlink(t)
	_, err := manager.ExportExactSkillRepository(context.Background(), repository, commit, contentpayload.Limits{
		MaxFiles: 10, MaxFileBytes: 1024, MaxTotalBytes: 4096, MaxManifestBytes: 4096,
	})
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.ErrorContains(t, err, "gitlink")
}

func newSkillMigrationRepositoryWithGitlink(t *testing.T) (GoGitRepositoryContentManager, repos.RepositoryIdentity, string) {
	t.Helper()
	manager, identity, _, repository := newExactSkillMigrationRepositoryForTest(t, map[string]string{"README.md": "skills"}, 0)
	seedRef, err := repository.Head()
	require.NoError(t, err)
	seedHash := seedRef.Hash()
	seed, err := repository.CommitObject(seedHash)
	require.NoError(t, err)
	seedTree, err := seed.Tree()
	require.NoError(t, err)
	readme, err := seedTree.FindEntry("README.md")
	require.NoError(t, err)
	badTree := object.Tree{Entries: []object.TreeEntry{
		{Name: "README.md", Mode: filemode.Regular, Hash: readme.Hash},
		{Name: "vendor", Mode: filemode.Submodule, Hash: seedHash},
	}}
	encodedTree := repository.Storer.NewEncodedObject()
	require.NoError(t, badTree.Encode(encodedTree))
	treeHash, err := repository.Storer.SetEncodedObject(encodedTree)
	require.NoError(t, err)
	badCommit := object.Commit{Author: seed.Author, Committer: seed.Committer, Message: "gitlink", TreeHash: treeHash}
	encodedCommit := repository.Storer.NewEncodedObject()
	require.NoError(t, badCommit.Encode(encodedCommit))
	badHash, err := repository.Storer.SetEncodedObject(encodedCommit)
	require.NoError(t, err)
	require.NoError(t, repository.Storer.SetReference(plumbing.NewHashReference(plumbing.NewBranchReferenceName("master"), badHash)))
	return manager, identity, badHash.String()
}

func newExactSkillMigrationRepositoryForTest(t *testing.T, files map[string]string, history int) (GoGitRepositoryContentManager, repos.RepositoryIdentity, string, *git.Repository) {
	t.Helper()
	root := t.TempDir()
	repositoryPath := filepath.Join(root, "usr_owner", "reviewer.sk.git")
	require.NoError(t, os.MkdirAll(repositoryPath, 0o755))
	repository, err := git.PlainInit(repositoryPath, false)
	require.NoError(t, err)
	worktree, err := repository.Worktree()
	require.NoError(t, err)
	for path, content := range files {
		fullPath := filepath.Join(repositoryPath, filepath.FromSlash(path))
		require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755))
		require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o644))
	}
	_, err = worktree.Add(".")
	require.NoError(t, err)
	commit, err := worktree.Commit("seed", &git.CommitOptions{Author: &object.Signature{Name: "test", Email: "test@example.com", When: time.Now()}})
	require.NoError(t, err)
	for index := 0; index < history; index++ {
		content := fmt.Sprintf("history-%02d", index)
		require.NoError(t, os.WriteFile(filepath.Join(repositoryPath, "README.md"), []byte(content), 0o644))
		_, err = worktree.Add("README.md")
		require.NoError(t, err)
		commit, err = worktree.Commit(content, &git.CommitOptions{Author: &object.Signature{Name: "test", Email: "test@example.com", When: time.Now().Add(time.Duration(index+1) * time.Second)}})
		require.NoError(t, err)
	}
	identityHandler := http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
		require.Equal(t, "/api/v3/repos/usr_owner/reviewer.sk", request.URL.Path)
		_ = json.NewEncoder(response).Encode(map[string]any{"id": 42})
	})
	manager := GoGitRepositoryContentManager{Handler: identityHandler, BaseURL: "file://localhost" + root}
	identity := repos.RepositoryIdentity{ID: "42", FullName: "usr_owner/reviewer.sk", DefaultBranch: "master"}
	return manager, identity, commit.String(), repository
}

func TestDisabledSkillContentMigrationHasNoJobOrAuthoritySideEffects(t *testing.T) {
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&migrationstore.Job{}, &contentstore.Authority{}))
	service := NewWithOptions(store.New(db), reposstore.New(db), nil, func() time.Time { return time.Now().UTC() }, ServiceOptions{})
	_, err = service.RunExactSkillContentMigration(context.Background(), "wsp_1", "usr_1", "skill_1", SkillContentMigrationRequest{
		RepositoryID: "42", CommitSHA: strings.Repeat("a", 40),
	})
	require.ErrorIs(t, err, ErrInvalidArgument)
	var jobs, authorities int64
	require.NoError(t, db.Model(&migrationstore.Job{}).Count(&jobs).Error)
	require.NoError(t, db.Model(&contentstore.Authority{}).Count(&authorities).Error)
	require.Zero(t, jobs)
	require.Zero(t, authorities)
}
