package contentpayload

import (
	"bytes"
	"context"
	"errors"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	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"
)

func TestGitTreeMigrationCopiesBareRepoAndProtectsExactCommit(t *testing.T) {
	work := filepath.Join(t.TempDir(), "work")
	require.NoError(t, os.Mkdir(work, 0o750))
	runGit(t, "init", work)
	runGit(t, "-C", work, "config", "user.name", "Migration Test")
	runGit(t, "-C", work, "config", "user.email", "migration@example.invalid")
	require.NoError(t, os.WriteFile(filepath.Join(work, "SKILL.md"), []byte("hello"), 0o640))
	runGit(t, "-C", work, "add", "SKILL.md")
	runGit(t, "-C", work, "commit", "-m", "seed")
	commit := runGit(t, "-C", work, "rev-parse", "HEAD")
	source := filepath.Join(t.TempDir(), "source.git")
	runGit(t, "clone", "--bare", work, source)
	require.NoError(t, os.WriteFile(filepath.Join(source, "hooks", "pre-receive"), []byte("exit 1"), 0o700))

	backendValue, err := NewGitTreeBackend(t.TempDir())
	require.NoError(t, err)
	backend := backendValue.(*gitTreeStorage)
	t.Cleanup(func() { require.NoError(t, backend.Close()) })
	require.NoError(t, backend.ImportRepository(context.Background(), ImportGitRepositoryRequest{WorkspaceID: "ws", ContentID: "ct", SourcePath: source, MaxFiles: 10_000, MaxBytes: 64 << 20}))
	refs := runGit(t, "--git-dir", filepath.Join(backend.root, "ws", "ct.git"), "for-each-ref", "--format=%(refname)")
	require.Empty(t, refs)
	staged, err := backend.ProtectImportedVersion(context.Background(), ProtectImportedGitVersionRequest{WorkspaceID: "ws", ContentID: "ct", VersionID: "v1", CommitSHA: commit})
	require.NoError(t, err)
	require.Contains(t, string(staged.ManifestRef), "@"+commit)
	require.NoError(t, backend.VerifyRepository(context.Background(), "ws", "ct"))
	entries, err := os.ReadDir(filepath.Join(backend.root, "ws", "ct.git", "hooks"))
	require.NoError(t, err)
	require.Empty(t, entries)
}

func TestGitTreeMigrationSyncFailureDoesNotPublishRepository(t *testing.T) {
	work := filepath.Join(t.TempDir(), "work")
	require.NoError(t, os.Mkdir(work, 0o750))
	runGit(t, "init", work)
	runGit(t, "-C", work, "config", "user.name", "Migration Test")
	runGit(t, "-C", work, "config", "user.email", "migration@example.invalid")
	require.NoError(t, os.WriteFile(filepath.Join(work, "a"), []byte("x"), 0o640))
	runGit(t, "-C", work, "add", "a")
	runGit(t, "-C", work, "commit", "-m", "seed")
	source := filepath.Join(t.TempDir(), "source.git")
	runGit(t, "clone", "--bare", work, source)
	root := t.TempDir()
	backendValue, err := NewGitTreeBackend(root)
	require.NoError(t, err)
	backend := backendValue.(*gitTreeStorage)
	t.Cleanup(func() { require.NoError(t, backend.Close()) })
	backend.syncPath = func(string, bool) error { return errors.New("injected import sync failure") }
	err = backend.ImportRepository(context.Background(), ImportGitRepositoryRequest{WorkspaceID: "ws", ContentID: "ct", SourcePath: source})
	require.ErrorContains(t, err, "sync imported content git")
	_, statErr := os.Stat(filepath.Join(root, "ws", "ct.git"))
	require.ErrorIs(t, statErr, os.ErrNotExist)
}

func TestGitTreeMigrationRootSyncFailureDoesNotPublishRepository(t *testing.T) {
	work := filepath.Join(t.TempDir(), "work")
	require.NoError(t, os.Mkdir(work, 0o750))
	runGit(t, "init", work)
	runGit(t, "-C", work, "config", "user.name", "Migration Test")
	runGit(t, "-C", work, "config", "user.email", "migration@example.invalid")
	require.NoError(t, os.WriteFile(filepath.Join(work, "a"), []byte("x"), 0o640))
	runGit(t, "-C", work, "add", "a")
	runGit(t, "-C", work, "commit", "-m", "seed")
	source := filepath.Join(t.TempDir(), "source.git")
	runGit(t, "clone", "--bare", work, source)
	root := t.TempDir()
	backendValue, err := NewGitTreeBackend(root)
	require.NoError(t, err)
	backend := backendValue.(*gitTreeStorage)
	t.Cleanup(func() { require.NoError(t, backend.Close()) })
	seen := make(map[string]bool)
	backend.syncFile = func(file *os.File, label string) error {
		seen[label] = true
		if label == "import-root" {
			return errors.New("injected import root sync failure")
		}
		return file.Sync()
	}
	err = backend.ImportRepository(context.Background(), ImportGitRepositoryRequest{WorkspaceID: "ws", ContentID: "ct", SourcePath: source})
	require.ErrorContains(t, err, "sync imported content repository root")
	require.True(t, seen["import-rollback-workspace"])
	require.True(t, seen["import-rollback-root"])
	_, statErr := os.Stat(filepath.Join(root, "ws", "ct.git"))
	require.ErrorIs(t, statErr, os.ErrNotExist)
	backend.syncFile = syncGitFile
	require.NoError(t, backend.ImportRepository(context.Background(), ImportGitRepositoryRequest{WorkspaceID: "ws", ContentID: "ct", SourcePath: source}))
	_, statErr = os.Stat(filepath.Join(root, "ws", "ct.git", gitRepositoryReadyMarker))
	require.NoError(t, statErr)
}

func TestGitTreeMigrationReadyMarkerFailureRollsBackDurablyAndRetries(t *testing.T) {
	work := filepath.Join(t.TempDir(), "work")
	require.NoError(t, os.Mkdir(work, 0o750))
	runGit(t, "init", work)
	runGit(t, "-C", work, "config", "user.name", "Migration Test")
	runGit(t, "-C", work, "config", "user.email", "migration@example.invalid")
	require.NoError(t, os.WriteFile(filepath.Join(work, "a"), []byte("x"), 0o640))
	runGit(t, "-C", work, "add", "a")
	runGit(t, "-C", work, "commit", "-m", "seed")
	source := filepath.Join(t.TempDir(), "source.git")
	runGit(t, "clone", "--bare", work, source)
	root := t.TempDir()
	backendValue, err := NewGitTreeBackend(root)
	require.NoError(t, err)
	backend := backendValue.(*gitTreeStorage)
	t.Cleanup(func() { require.NoError(t, backend.Close()) })
	seen := make(map[string]bool)
	backend.syncFile = func(file *os.File, label string) error {
		seen[label] = true
		if label == "ready-marker" {
			return errors.New("injected ready marker sync failure")
		}
		return file.Sync()
	}
	err = backend.ImportRepository(context.Background(), ImportGitRepositoryRequest{WorkspaceID: "ws", ContentID: "ct", SourcePath: source})
	require.ErrorContains(t, err, "sync content git ready marker")
	require.True(t, seen["import-rollback-workspace"])
	require.True(t, seen["import-rollback-root"])
	_, statErr := os.Stat(filepath.Join(root, "ws", "ct.git"))
	require.ErrorIs(t, statErr, os.ErrNotExist)
	backend.syncFile = syncGitFile
	require.NoError(t, backend.ImportRepository(context.Background(), ImportGitRepositoryRequest{WorkspaceID: "ws", ContentID: "ct", SourcePath: source}))
}

func TestGitTreeMigrationRollbackSyncFailureIsVisible(t *testing.T) {
	work := filepath.Join(t.TempDir(), "work")
	require.NoError(t, os.Mkdir(work, 0o750))
	runGit(t, "init", work)
	runGit(t, "-C", work, "config", "user.name", "Migration Test")
	runGit(t, "-C", work, "config", "user.email", "migration@example.invalid")
	require.NoError(t, os.WriteFile(filepath.Join(work, "a"), []byte("x"), 0o640))
	runGit(t, "-C", work, "add", "a")
	runGit(t, "-C", work, "commit", "-m", "seed")
	source := filepath.Join(t.TempDir(), "source.git")
	runGit(t, "clone", "--bare", work, source)
	backendValue, err := NewGitTreeBackend(t.TempDir())
	require.NoError(t, err)
	backend := backendValue.(*gitTreeStorage)
	t.Cleanup(func() { require.NoError(t, backend.Close()) })
	backend.syncFile = func(file *os.File, label string) error {
		switch label {
		case "import-root":
			return errors.New("injected import root sync failure")
		case "import-rollback-workspace":
			return errors.New("injected rollback sync failure")
		default:
			return file.Sync()
		}
	}
	err = backend.ImportRepository(context.Background(), ImportGitRepositoryRequest{WorkspaceID: "ws", ContentID: "ct", SourcePath: source})
	require.ErrorContains(t, err, "sync imported content repository root")
	require.ErrorContains(t, err, "sync imported content repository rollback workspace")
}

func TestGitTreeMigrationRejectsAlternates(t *testing.T) {
	source := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(source, "objects", "info"), 0o750))
	require.NoError(t, os.WriteFile(filepath.Join(source, "objects", "info", "alternates"), []byte("/external"), 0o640))
	backendValue, err := NewGitTreeBackend(t.TempDir())
	require.NoError(t, err)
	backend := backendValue.(*gitTreeStorage)
	err = backend.ImportRepository(context.Background(), ImportGitRepositoryRequest{WorkspaceID: "ws", ContentID: "ct", SourcePath: source})
	require.ErrorContains(t, err, "not self-contained")
}

func TestGitTreeMigrationRejectsReplaceRefsAndBoundsBatch(t *testing.T) {
	source := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(source, "refs", "replace"), 0o750))
	backendValue, err := NewGitTreeBackend(t.TempDir())
	require.NoError(t, err)
	backend := backendValue.(*gitTreeStorage)
	t.Cleanup(func() { require.NoError(t, backend.Close()) })
	err = backend.ImportRepository(context.Background(), ImportGitRepositoryRequest{WorkspaceID: "ws", ContentID: "ct", SourcePath: source})
	require.ErrorContains(t, err, "not self-contained")

	requests := make([]ProtectImportedGitVersionRequest, MaxGitMigrationProtectBatch+1)
	_, err = backend.ProtectImportedVersions(context.Background(), requests, 1)
	require.ErrorContains(t, err, "batch")
	_, err = backend.ProtectImportedVersions(context.Background(), requests[:1], MaxGitMigrationConcurrency+1)
	require.ErrorContains(t, err, "concurrency")
}

func TestGitTreeRuntimeRejectsConfigRedirect(t *testing.T) {
	root := t.TempDir()
	backendValue, err := NewGitTreeBackend(root)
	require.NoError(t, err)
	backend := backendValue.(*gitTreeStorage)
	t.Cleanup(func() { require.NoError(t, backend.Close()) })
	staged, err := backend.StageTree(context.Background(), StageTreeRequest{WorkspaceID: "ws", ContentID: "ct", VersionID: "v1", Files: []File{{Path: "a", Size: 1, Content: bytes.NewBufferString("x")}}})
	require.NoError(t, err)
	config := filepath.Join(root, "ws", "ct.git", "config")
	file, err := os.OpenFile(config, os.O_APPEND|os.O_WRONLY, 0)
	require.NoError(t, err)
	_, err = file.WriteString("[include]\n\tpath = /tmp/evil\n")
	require.NoError(t, err)
	require.NoError(t, file.Close())
	_, err = backend.OpenManifest(context.Background(), staged.ManifestRef)
	require.ErrorContains(t, err, "forbidden section")
}

func TestGitTreeProtectImportedVersionValidatesBeforeRefPublish(t *testing.T) {
	work := filepath.Join(t.TempDir(), "work")
	require.NoError(t, os.Mkdir(work, 0o750))
	runGit(t, "init", work)
	runGit(t, "-C", work, "config", "user.name", "Migration Test")
	runGit(t, "-C", work, "config", "user.email", "migration@example.invalid")
	require.NoError(t, os.WriteFile(filepath.Join(work, "a"), []byte("x"), 0o640))
	runGit(t, "-C", work, "add", "a")
	runGit(t, "-C", work, "commit", "-m", "seed")
	source := filepath.Join(t.TempDir(), "source.git")
	runGit(t, "clone", "--bare", work, source)
	repo, err := git.PlainOpen(source)
	require.NoError(t, err)
	seed, err := repo.CommitObject(plumbing.NewHash(runGit(t, "--git-dir", source, "rev-parse", "HEAD")))
	require.NoError(t, err)
	seedTree, err := seed.Tree()
	require.NoError(t, err)
	seedFile, err := seedTree.File("a")
	require.NoError(t, err)
	badTree := object.Tree{Entries: []object.TreeEntry{{Name: "bad", Mode: filemode.Symlink, Hash: seedFile.Hash}}}
	encoded := repo.Storer.NewEncodedObject()
	require.NoError(t, badTree.Encode(encoded))
	treeHash, err := repo.Storer.SetEncodedObject(encoded)
	require.NoError(t, err)
	badCommit := object.Commit{Author: seed.Author, Committer: seed.Committer, Message: "bad", TreeHash: treeHash}
	encoded = repo.Storer.NewEncodedObject()
	require.NoError(t, badCommit.Encode(encoded))
	badHash, err := repo.Storer.SetEncodedObject(encoded)
	require.NoError(t, err)
	require.NoError(t, repo.Storer.SetReference(plumbing.NewHashReference("refs/tags/bad", badHash)))

	backendValue, err := NewGitTreeBackend(t.TempDir())
	require.NoError(t, err)
	backend := backendValue.(*gitTreeStorage)
	t.Cleanup(func() { require.NoError(t, backend.Close()) })
	require.NoError(t, backend.ImportRepository(context.Background(), ImportGitRepositoryRequest{WorkspaceID: "ws", ContentID: "ct", SourcePath: source}))
	_, err = backend.ProtectImportedVersion(context.Background(), ProtectImportedGitVersionRequest{WorkspaceID: "ws", ContentID: "ct", VersionID: "v1", CommitSHA: badHash.String()})
	require.ErrorContains(t, err, "unsupported entry")
	_, err = os.Stat(filepath.Join(backend.root, "ws", "ct.git", "refs", "content-versions", "v1"))
	require.ErrorIs(t, err, os.ErrNotExist)
	staged, err := backend.ProtectImportedVersion(context.Background(), ProtectImportedGitVersionRequest{WorkspaceID: "ws", ContentID: "ct", VersionID: "v1", CommitSHA: seed.Hash.String()})
	require.NoError(t, err)
	require.True(t, staged.Created)
}

func TestGitTreeMigrationRejectsPackedReservedRefs(t *testing.T) {
	work := filepath.Join(t.TempDir(), "work")
	require.NoError(t, os.Mkdir(work, 0o750))
	runGit(t, "init", work)
	runGit(t, "-C", work, "config", "user.name", "Migration Test")
	runGit(t, "-C", work, "config", "user.email", "migration@example.invalid")
	require.NoError(t, os.WriteFile(filepath.Join(work, "a"), []byte("x"), 0o640))
	runGit(t, "-C", work, "add", "a")
	runGit(t, "-C", work, "commit", "-m", "seed")
	source := filepath.Join(t.TempDir(), "source.git")
	runGit(t, "clone", "--bare", work, source)
	sha := runGit(t, "--git-dir", source, "rev-parse", "HEAD")
	for _, ref := range []string{"refs/replace/" + strings.Repeat("1", 40), "refs/content-versions/forged"} {
		t.Run(strings.Split(ref, "/")[1], func(t *testing.T) {
			copy := filepath.Join(t.TempDir(), "copy.git")
			require.NoError(t, copyDirectory(source, copy))
			runGit(t, "--git-dir", copy, "update-ref", ref, sha)
			runGit(t, "--git-dir", copy, "pack-refs", "--all")
			backendValue, err := NewGitTreeBackend(t.TempDir())
			require.NoError(t, err)
			backend := backendValue.(*gitTreeStorage)
			err = backend.ImportRepository(context.Background(), ImportGitRepositoryRequest{WorkspaceID: "ws", ContentID: "ct", SourcePath: copy})
			require.Error(t, err)
		})
	}
}

func copyDirectory(source, target string) error {
	return filepath.WalkDir(source, func(path string, entry os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		relative, err := filepath.Rel(source, path)
		if err != nil {
			return err
		}
		destination := filepath.Join(target, relative)
		if entry.IsDir() {
			return os.MkdirAll(destination, 0o750)
		}
		data, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		return os.WriteFile(destination, data, 0o640)
	})
}

func runGit(t *testing.T, args ...string) string {
	t.Helper()
	command := exec.Command("git", args...)
	command.Env = append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "GIT_TERMINAL_PROMPT=0")
	output, err := command.CombinedOutput()
	require.NoError(t, err, "%s", output)
	return string(bytes.TrimSpace(output))
}
