package memory

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

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
)

func TestPreflightMemoryMigrationRetainsFirstParentBackupsAndReferences(t *testing.T) {
	root := migrationTestSHA(1)
	middle := migrationTestSHA(2)
	head := migrationTestSHA(3)
	// Give the child the lexically smaller SHA to prove detached versions are
	// ordered by retained ancestry rather than object ID.
	detached := migrationTestSHA(4)
	detachedParent := migrationTestSHA(5)
	source := &fakeMemoryMigrationSource{
		head: head,
		backups: []MemoryMigrationReference{
			{Kind: "backup", Label: "before", CommitSHA: middle},
			{Kind: "backup", Label: "detached", CommitSHA: detached},
		},
		commits: map[string]MemoryMigrationCommit{
			root:           {CommitSHA: root},
			middle:         {CommitSHA: middle, FirstParentSHA: root},
			head:           {CommitSHA: head, FirstParentSHA: middle},
			detachedParent: {CommitSHA: detachedParent, FirstParentSHA: middle},
			detached:       {CommitSHA: detached, FirstParentSHA: detachedParent},
		},
		snapshots: map[string][]MemoryMigrationFile{
			root:           {{Path: "MEMORY.md", Data: []byte("root\n")}},
			middle:         {{Path: "MEMORY.md", Data: []byte("middle\n")}},
			head:           {{Path: "MEMORY.md", Data: []byte("head\n")}},
			detachedParent: {{Path: "MEMORY.md", Data: []byte("detached parent\n")}},
			detached:       {{Path: "MEMORY.md", Data: []byte("detached\n")}},
		},
	}
	references := []MemoryMigrationReference{
		{Kind: "product", Label: "active", CommitSHA: head},
		{Kind: "product", Label: "detached-parent", CommitSHA: detachedParent},
	}
	plan, err := PreflightMemoryMigration(context.Background(), source, MemoryMigrationPreflightRequest{
		Repository:        repos.RepositoryIdentity{ID: "repo_1", DefaultBranch: "main"},
		ProductReferences: references,
	})
	require.NoError(t, err)
	require.Equal(t, head, plan.HeadCommitSHA)
	require.EqualValues(t, 5, plan.VersionCount)
	require.EqualValues(t, 2, plan.PinCount)
	require.Len(t, plan.References, 4)
	require.Equal(t, []string{root, middle, head, detachedParent, detached}, migrationPlanSHAs(plan.Versions))
	require.Equal(t, []uint64{3, 4, 5, 1, 2}, migrationPlanHistorySequences(plan.Versions))
	require.Equal(t, "", plan.Versions[0].ParentCommitSHA)
	require.Equal(t, root, plan.Versions[1].ParentCommitSHA)
	require.Equal(t, middle, plan.Versions[2].ParentCommitSHA)
	require.Equal(t, middle, plan.Versions[3].ParentCommitSHA)
	require.Equal(t, detachedParent, plan.Versions[4].ParentCommitSHA)
	require.False(t, plan.Versions[3].FirstParent)
	require.False(t, plan.Versions[4].FirstParent)
	require.Equal(t, []string{root, middle, head, detachedParent, detached}, source.snapshotReads)
}

func TestPreflightMemoryMigrationFailsClosedWithoutTruncation(t *testing.T) {
	t.Run("history quota", func(t *testing.T) {
		source := migrationLinearSource(int(memoryContentMaxVersions) + 1)
		_, err := PreflightMemoryMigration(context.Background(), source, MemoryMigrationPreflightRequest{
			Repository: repos.RepositoryIdentity{ID: "repo_1", DefaultBranch: "main"},
		})
		require.ErrorContains(t, err, "first-parent history exceeds")
		require.Empty(t, source.snapshotReads, "over-limit inventory must fail before payload export")
	})

	t.Run("backup pin quota", func(t *testing.T) {
		source := migrationLinearSource(1)
		for index := 0; index <= memoryMigrationMaxBackupReferences; index++ {
			source.backups = append(source.backups, MemoryMigrationReference{Kind: "backup", Label: fmt.Sprintf("backup-%d", index), CommitSHA: source.head})
		}
		_, err := PreflightMemoryMigration(context.Background(), source, MemoryMigrationPreflightRequest{
			Repository: repos.RepositoryIdentity{ID: "repo_1", DefaultBranch: "main"},
		})
		require.ErrorContains(t, err, "backup labels exceed")
		require.Empty(t, source.snapshotReads)
	})

	t.Run("product references cannot bypass backup pin quota", func(t *testing.T) {
		source := migrationLinearSource(1)
		references := make([]MemoryMigrationReference, 0, memoryMigrationMaxBackupReferences+1)
		for index := 0; index <= memoryMigrationMaxBackupReferences; index++ {
			references = append(references, MemoryMigrationReference{Kind: "backup", Label: fmt.Sprintf("product-backup-%d", index), CommitSHA: source.head})
		}
		_, err := PreflightMemoryMigration(context.Background(), source, MemoryMigrationPreflightRequest{
			Repository: repos.RepositoryIdentity{ID: "repo_1", DefaultBranch: "main"}, ProductReferences: references,
		})
		require.ErrorContains(t, err, "backup labels exceed")
		require.Empty(t, source.snapshotReads)
	})

	t.Run("non text snapshot", func(t *testing.T) {
		source := migrationLinearSource(1)
		source.snapshots[source.head] = []MemoryMigrationFile{{Path: "MEMORY.md", Data: []byte{'a', 0, 'b'}}}
		_, err := PreflightMemoryMigration(context.Background(), source, MemoryMigrationPreflightRequest{
			Repository: repos.RepositoryIdentity{ID: "repo_1", DefaultBranch: "main"},
		})
		require.ErrorContains(t, err, "non-text")
	})

	t.Run("normalization collision", func(t *testing.T) {
		source := migrationLinearSource(1)
		source.snapshots[source.head] = []MemoryMigrationFile{{Path: "MEMORY.md", Data: []byte("one")}, {Path: "MEMORY.md", Data: []byte("two")}}
		_, err := PreflightMemoryMigration(context.Background(), source, MemoryMigrationPreflightRequest{
			Repository: repos.RepositoryIdentity{ID: "repo_1", DefaultBranch: "main"},
		})
		require.ErrorContains(t, err, "normalization collision")
	})

	t.Run("source metadata", func(t *testing.T) {
		source := migrationLinearSource(1)
		commit := source.commits[source.head]
		commit.AuthoredAt = "not-a-time"
		source.commits[source.head] = commit
		_, err := PreflightMemoryMigration(context.Background(), source, MemoryMigrationPreflightRequest{
			Repository: repos.RepositoryIdentity{ID: "repo_1", DefaultBranch: "main"},
		})
		require.ErrorContains(t, err, "authored timestamp is invalid")
		require.Empty(t, source.snapshotReads)

		commit.AuthoredAt = time.Now().UTC().Format(time.RFC3339Nano)
		commit.Message = strings.Repeat("m", memoryMigrationMaxMessageBytes+1)
		source.commits[source.head] = commit
		_, err = PreflightMemoryMigration(context.Background(), source, MemoryMigrationPreflightRequest{
			Repository: repos.RepositoryIdentity{ID: "repo_1", DefaultBranch: "main"},
		})
		require.ErrorContains(t, err, "metadata exceeds bounds")
		require.Empty(t, source.snapshotReads)
	})
}

type fakeMemoryMigrationSource struct {
	head          string
	backups       []MemoryMigrationReference
	commits       map[string]MemoryMigrationCommit
	snapshots     map[string][]MemoryMigrationFile
	snapshotReads []string
	snapshotHook  func(int, repos.RepositoryIdentity)
}

func (source *fakeMemoryMigrationSource) ResolveMemoryMigrationHead(context.Context, repos.RepositoryIdentity, string) (string, error) {
	return source.head, nil
}

func (source *fakeMemoryMigrationSource) ListMemoryMigrationBackups(_ context.Context, _ repos.RepositoryIdentity, limit int) ([]MemoryMigrationReference, error) {
	if len(source.backups) > limit {
		return append([]MemoryMigrationReference(nil), source.backups[:limit]...), nil
	}
	return append([]MemoryMigrationReference(nil), source.backups...), nil
}

func (source *fakeMemoryMigrationSource) ReadMemoryMigrationCommit(_ context.Context, _ repos.RepositoryIdentity, sha string) (MemoryMigrationCommit, error) {
	commit, ok := source.commits[sha]
	if !ok {
		return MemoryMigrationCommit{}, fmt.Errorf("missing commit %s", sha)
	}
	if commit.AuthorName == "" {
		commit.AuthorName = "Memory Author"
	}
	if commit.AuthorEmail == "" {
		commit.AuthorEmail = "memory-author@example.test"
	}
	if commit.AuthoredAt == "" {
		commit.AuthoredAt = time.Date(2026, 1, 1, 0, 0, len(source.snapshotReads), 0, time.UTC).Format(time.RFC3339Nano)
	}
	return commit, nil
}

func (source *fakeMemoryMigrationSource) ReadMemoryMigrationSnapshot(_ context.Context, repository repos.RepositoryIdentity, sha string, _ contentpayload.Limits) ([]MemoryMigrationFile, error) {
	source.snapshotReads = append(source.snapshotReads, sha)
	if source.snapshotHook != nil {
		source.snapshotHook(len(source.snapshotReads), repository)
	}
	files, ok := source.snapshots[sha]
	if !ok {
		return nil, fmt.Errorf("missing snapshot %s", sha)
	}
	return append([]MemoryMigrationFile(nil), files...), nil
}

func migrationLinearSource(count int) *fakeMemoryMigrationSource {
	source := &fakeMemoryMigrationSource{commits: make(map[string]MemoryMigrationCommit, count), snapshots: make(map[string][]MemoryMigrationFile, count)}
	parent := ""
	for index := 1; index <= count; index++ {
		sha := migrationTestSHA(index)
		source.commits[sha] = MemoryMigrationCommit{CommitSHA: sha, FirstParentSHA: parent}
		source.snapshots[sha] = []MemoryMigrationFile{{Path: "MEMORY.md", Data: []byte(fmt.Sprintf("%d\n", index))}}
		parent = sha
	}
	source.head = parent
	return source
}

func migrationTestSHA(value int) string { return fmt.Sprintf("%040x", value) }

func migrationPlanSHAs(versions []MemoryMigrationPlanVersion) []string {
	result := make([]string, 0, len(versions))
	for _, version := range versions {
		result = append(result, version.CommitSHA)
	}
	return result
}

func migrationPlanHistorySequences(versions []MemoryMigrationPlanVersion) []uint64 {
	result := make([]uint64, 0, len(versions))
	for _, version := range versions {
		result = append(result, version.HistorySequence)
	}
	return result
}
