package content

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	store "github.com/tidbcloud/AgentTeam/internal/store/content"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func newServiceTest(t *testing.T) (*Service, *gorm.DB) {
	t.Helper()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	closeServiceTestDB(t, db)
	require.NoError(t, db.AutoMigrate(&store.VersionedContent{}, &store.ContentVersion{}, &store.VersionOperation{}, &store.VersionPath{}, &store.VersionPin{}, &store.Outbox{}, &store.StagedPayloadGC{}))
	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	document, err := contentpayload.NewDocumentBackend(tree)
	require.NoError(t, err)
	sequence := 0
	service, err := New(store.New(db), tree, document, func(prefix string) (string, error) {
		sequence++
		return fmt.Sprintf("%s_%d", prefix, sequence), nil
	})
	require.NoError(t, err)
	service.now = func() time.Time { return time.Date(2026, 8, 10, 20, 0, 0, 0, time.UTC) }
	return service, db
}

func newGitServiceTest(t *testing.T) (*Service, *gorm.DB, string) {
	t.Helper()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	closeServiceTestDB(t, db)
	require.NoError(t, db.AutoMigrate(&store.VersionedContent{}, &store.ContentVersion{}, &store.VersionOperation{}, &store.VersionPath{}, &store.VersionPin{}, &store.Outbox{}, &store.StagedPayloadGC{}))
	root := t.TempDir()
	tree, err := contentpayload.NewGitTreeBackend(root)
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	documentTree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, documentTree.Close()) })
	document, err := contentpayload.NewDocumentBackend(documentTree)
	require.NoError(t, err)
	sequence := 0
	service, err := New(store.New(db), tree, document, func(prefix string) (string, error) { sequence++; return fmt.Sprintf("%s_%d", prefix, sequence), nil })
	require.NoError(t, err)
	service.now = func() time.Time { return time.Date(2026, 8, 10, 20, 0, 0, 0, time.UTC) }
	return service, db, root
}

func closeServiceTestDB(t *testing.T, db *gorm.DB) {
	t.Helper()
	sqlDB, err := db.DB()
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
}

func TestOpenTreeArchiveFailsClosedAfterExactRefTamperOrRemoval(t *testing.T) {
	for _, mode := range []string{"tamper", "remove"} {
		t.Run(mode, func(t *testing.T) {
			service, database, root := newGitServiceTest(t)
			content, err := service.CreateContent(context.Background(), CreateContentRequest{WorkspaceID: "wsp_1", Domain: "skill", BindingKey: "skill_1", Model: store.ModelTree, Actor: "usr_1"})
			require.NoError(t, err)
			published, err := service.CommitVersion(context.Background(), CommitVersionRequest{WorkspaceID: "wsp_1", ContentID: content.ID, ExpectedGeneration: 1, Actor: "usr_1", Model: store.ModelTree, Files: []contentpayload.File{{Path: "SKILL.md", Size: 5, Content: bytes.NewBufferString("hello")}}})
			require.NoError(t, err)
			var stored store.ContentVersion
			require.NoError(t, database.Where("workspace_id = ? AND content_id = ? AND id = ?", "wsp_1", content.ID, published.Version.ID).First(&stored).Error)
			_, commit, ok := strings.Cut(stored.PayloadRef, "@")
			require.True(t, ok)
			refPath := filepath.Join(root, "wsp_1", content.ID+".git", "refs", "content-versions", published.Version.ID)
			if mode == "remove" {
				require.NoError(t, os.Remove(refPath))
			} else {
				require.NoError(t, os.WriteFile(refPath, []byte(strings.Repeat("0", len(commit))+"\n"), 0o640))
			}
			reader, _, err := service.OpenTreeArchive(context.Background(), "wsp_1", content.ID, published.Version.ID, ArchiveLimits{MaxFiles: 10, MaxFileBytes: 1 << 20, MaxTotalBytes: 1 << 20})
			require.Error(t, err)
			require.Nil(t, reader)
		})
	}
}

func TestTreeContentCreateCommitReadHistoryAndCompare(t *testing.T) {
	service, _ := newServiceTest(t)
	ctx := context.Background()
	content, err := service.CreateContent(ctx, CreateContentRequest{WorkspaceID: "wsp_1", Domain: "skill", BindingKey: "skill_1", Model: store.ModelTree, Actor: "usr_1"})
	require.NoError(t, err)

	first, err := service.CommitVersion(ctx, CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: content.ID, ExpectedGeneration: 1, Actor: "usr_1", Model: store.ModelTree,
		Files: []contentpayload.File{{Path: "SKILL.md", Size: 5, Content: bytes.NewBufferString("hello")}},
	})
	require.NoError(t, err)
	require.Equal(t, first.Version.ID, first.Content.HeadVersionID)

	second, err := service.CommitVersion(ctx, CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: content.ID, ExpectedHeadVersionID: first.Version.ID, ExpectedGeneration: 1, Actor: "usr_1", Model: store.ModelTree,
		Files: []contentpayload.File{
			{Path: "SKILL.md", Size: 7, Content: bytes.NewBufferString("hello 2")},
			{Path: "refs/info.md", Size: 4, Content: bytes.NewBufferString("info")},
		},
	})
	require.NoError(t, err)

	body, meta, err := service.OpenFile(ctx, "wsp_1", content.ID, second.Version.ID, "SKILL.md", nil)
	require.NoError(t, err)
	defer body.Close()
	got, err := io.ReadAll(body)
	require.NoError(t, err)
	require.Equal(t, "hello 2", string(got))
	require.Equal(t, int64(7), meta.SizeBytes)

	history, err := service.PathHistory(ctx, "wsp_1", content.ID, "SKILL.md", 10)
	require.NoError(t, err)
	require.Len(t, history, 2)

	diff, err := service.Compare(ctx, "wsp_1", content.ID, first.Version.ID, second.Version.ID)
	require.NoError(t, err)
	require.Len(t, diff, 2)
	require.Equal(t, "modified", diff[0].Change)
	require.Equal(t, "added", diff[1].Change)
	_, _, err = service.OpenTreeArchive(ctx, "wsp_1", content.ID, second.Version.ID, ArchiveLimits{MaxFiles: 1, MaxFileBytes: 1 << 20, MaxTotalBytes: 1 << 20})
	require.ErrorContains(t, err, "exceeds request bounds")
}

func TestCreateContentQuotaProfileIsCompleteAndAtomic(t *testing.T) {
	service, database := newServiceTest(t)
	ctx := context.Background()

	defaults, err := service.CreateContent(ctx, CreateContentRequest{
		WorkspaceID: "wsp_1", Domain: "skill", BindingKey: "skill_default",
		Model: store.ModelTree, Actor: "usr_1",
	})
	require.NoError(t, err)
	require.Equal(t, store.DefaultTreeVersions, defaults.MaxRetainedVersions)
	require.Equal(t, store.DefaultTreeBytes, defaults.MaxStoredPayloadBytes)
	require.Equal(t, store.DefaultTreePins, defaults.MaxPins)

	profile := QuotaProfile{MaxRetainedVersions: 512, MaxStoredPayloadBytes: 1 << 30, MaxPins: 64}
	explicit, err := service.CreateContent(ctx, CreateContentRequest{
		WorkspaceID: "wsp_1", Domain: "memory", BindingKey: "memory_1",
		Model: store.ModelTree, Actor: "usr_1", Quota: profile,
	})
	require.NoError(t, err)
	require.Equal(t, profile.MaxRetainedVersions, explicit.MaxRetainedVersions)
	require.Equal(t, profile.MaxStoredPayloadBytes, explicit.MaxStoredPayloadBytes)
	require.Equal(t, profile.MaxPins, explicit.MaxPins)
	var stored store.VersionedContent
	require.NoError(t, database.Where("workspace_id = ? AND id = ?", "wsp_1", explicit.ID).First(&stored).Error)
	require.Equal(t, profile.MaxRetainedVersions, stored.MaxRetainedVersions)
	require.Equal(t, profile.MaxStoredPayloadBytes, stored.MaxStoredPayloadBytes)
	require.Equal(t, profile.MaxPins, stored.MaxPins)

	_, err = service.CreateContent(ctx, CreateContentRequest{
		WorkspaceID: "wsp_1", Domain: "memory", BindingKey: "memory_partial",
		Model: store.ModelTree, Actor: "usr_1",
		Quota: QuotaProfile{MaxRetainedVersions: 512, MaxStoredPayloadBytes: 1 << 30},
	})
	require.ErrorContains(t, err, "fully defaulted or fully explicit")
	var count int64
	require.NoError(t, database.Model(&store.VersionedContent{}).
		Where("workspace_id = ? AND domain = ? AND binding_key = ?", "wsp_1", "memory", "memory_partial").
		Count(&count).Error)
	require.Zero(t, count)
}

func TestServiceRejectsListLimitsAboveContract(t *testing.T) {
	service, _ := newServiceTest(t)
	ctx := context.Background()

	_, err := service.ListVersions(ctx, "wsp_1", "ctn_1", MaxVersionListRows+1)
	require.ErrorContains(t, err, "version list limit")
	_, err = service.PathHistory(ctx, "wsp_1", "ctn_1", "SKILL.md", MaxPathHistoryRows+1)
	require.ErrorContains(t, err, "path history limit")
	_, err = service.ListPins(ctx, "wsp_1", "ctn_1", MaxPinListRows+1)
	require.ErrorContains(t, err, "pin list limit")
}

func TestActiveTreeServiceRejectsLegacyDocumentLineage(t *testing.T) {
	legacy, database := newServiceTest(t)
	document, err := legacy.CreateContent(context.Background(), CreateContentRequest{
		WorkspaceID: "wsp_1", Domain: "memory", BindingKey: "mem_1",
		Model: store.ModelDocument, Actor: "usr_1",
	})
	require.NoError(t, err)

	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	active, err := NewTree(store.New(database), tree, func(prefix string) (string, error) { return prefix + "_active", nil })
	require.NoError(t, err)

	_, err = active.GetContent(context.Background(), "wsp_1", document.ID)
	require.ErrorIs(t, err, store.ErrModelConflict)
	_, found, err := active.FindContentByBinding(context.Background(), "wsp_1", "memory", "mem_1")
	require.False(t, found)
	require.ErrorIs(t, err, store.ErrModelConflict)
}

func TestDocumentCheckpointIsFullSnapshotAndQuotaFailureKeepsHead(t *testing.T) {
	service, database := newServiceTest(t)
	ctx := context.Background()
	content, err := service.CreateContent(ctx, CreateContentRequest{WorkspaceID: "wsp_1", Domain: "memory", BindingKey: "mem_1", Model: store.ModelDocument, Actor: "usr_1"})
	require.NoError(t, err)
	require.Equal(t, store.DefaultDocumentVersions, content.MaxRetainedVersions)

	first, err := service.CommitVersion(ctx, CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: content.ID, ExpectedGeneration: 1, Actor: "usr_1", Model: store.ModelDocument, Checkpoint: true,
		Files: []contentpayload.File{
			{Path: "MEMORY.md", Size: 6, Content: bytes.NewBufferString("memory")},
			{Path: "state/open-loops.json", Size: 2, Content: bytes.NewBufferString("[]")},
		},
	})
	require.NoError(t, err)
	require.True(t, first.Version.Checkpoint)
	_, _, err = service.OpenTreeArchive(ctx, "wsp_1", content.ID, first.Version.ID, ArchiveLimits{MaxFiles: 10, MaxFileBytes: 1 << 20, MaxTotalBytes: 16 << 20})
	require.ErrorIs(t, err, store.ErrModelConflict)

	var stored store.VersionedContent
	require.NoError(t, database.Where("workspace_id = ? AND id = ?", "wsp_1", content.ID).First(&stored).Error)
	stored.MaxStoredPayloadBytes = stored.StoredPayloadBytes
	require.NoError(t, database.Model(&store.VersionedContent{}).Where("id = ?", content.ID).Update("max_stored_payload_bytes", stored.MaxStoredPayloadBytes).Error)

	_, err = service.CommitVersion(ctx, CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: content.ID, ExpectedHeadVersionID: first.Version.ID, ExpectedGeneration: 1, Actor: "usr_1", Model: store.ModelDocument, Checkpoint: true,
		Files: []contentpayload.File{{Path: "MEMORY.md", Size: 7, Content: bytes.NewBufferString("memory2")}},
	})
	require.ErrorIs(t, err, store.ErrQuotaExceeded)
	after, err := service.GetContent(ctx, "wsp_1", content.ID)
	require.NoError(t, err)
	require.Equal(t, first.Version.ID, after.HeadVersionID)
}
