package content

import (
	"bytes"
	"context"
	"errors"
	"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 TestGCWorkerDeletesOnlyUnreachableStagedPayload(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.VersionedContent{}, &store.ContentVersion{}, &store.StagedPayloadGC{}))
	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	now := time.Date(2026, 8, 11, 10, 0, 0, 0, time.UTC)

	staged, err := tree.StageTree(ctx, contentpayload.StageTreeRequest{
		WorkspaceID: "wsp_1", ContentID: "ctn_1", VersionID: "ctv_1",
		Files: []contentpayload.File{{Path: "SKILL.md", Size: 5, Content: bytes.NewBufferString("hello")}},
	})
	require.NoError(t, err)
	contentStore := store.New(db)
	require.NoError(t, db.Create(&store.VersionedContent{ID: "ctn_1", WorkspaceID: "wsp_1", Domain: "test_tree", BindingKey: "fixture_1", Model: store.ModelTree, MaxRetainedVersions: 1, MaxStoredPayloadBytes: 1, MaxPins: 1, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now}).Error)
	require.NoError(t, contentStore.RegisterStagedPayloadGC(ctx, store.StagedPayloadGC{
		ID: "ctg_1", WorkspaceID: "wsp_1", ContentID: "ctn_1", VersionID: "ctv_1", Model: store.ModelTree,
		PayloadRef: string(staged.ManifestRef), DeleteAfter: now,
	}))
	worker, err := NewScopedTreeGCWorker(contentStore, tree, GCBindingScope{WorkspaceID: "wsp_1", Domain: "test_tree", BindingKey: "fixture_1"}, "worker-1", time.Minute)
	require.NoError(t, err)
	worker.now = func() time.Time { return now }

	result, err := worker.RunOnce(ctx, 10)
	require.NoError(t, err)
	require.Equal(t, GCWorkerResult{Leased: 1, Deleted: 1}, result)
	_, err = tree.OpenManifest(ctx, staged.ManifestRef)
	require.True(t, errors.Is(err, contentpayload.ErrNotFound), err)
	var gc store.StagedPayloadGC
	require.NoError(t, db.Where("id = ?", "ctg_1").First(&gc).Error)
	require.Equal(t, store.GCStatusDeleted, gc.Status)
}

func TestGCWorkerPreservesReachableStagedPayload(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.VersionedContent{}, &store.ContentVersion{}, &store.StagedPayloadGC{}))
	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	now := time.Date(2026, 8, 11, 10, 0, 0, 0, time.UTC)

	staged, err := tree.StageTree(ctx, contentpayload.StageTreeRequest{
		WorkspaceID: "wsp_1", ContentID: "ctn_1", VersionID: "ctv_1",
		Files: []contentpayload.File{{Path: "SKILL.md", Size: 5, Content: bytes.NewBufferString("hello")}},
	})
	require.NoError(t, err)
	require.NoError(t, db.Create(&store.VersionedContent{ID: "ctn_1", WorkspaceID: "wsp_1", Domain: "test_tree", BindingKey: "fixture_1", Model: store.ModelTree, MaxRetainedVersions: 1, MaxStoredPayloadBytes: 1, MaxPins: 1, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now}).Error)
	require.NoError(t, db.Create(&store.ContentVersion{
		ID: "ctv_1", WorkspaceID: "wsp_1", ContentID: "ctn_1", Generation: 1, Model: store.ModelTree,
		PayloadRef: string(staged.ManifestRef), RootDigest: string(staged.RootDigest), SizeBytes: staged.SizeBytes, FileCount: staged.FileCount,
		CreatedBy: "usr_1", CreatedAt: now,
	}).Error)
	contentStore := store.New(db)
	require.NoError(t, contentStore.RegisterStagedPayloadGC(ctx, store.StagedPayloadGC{
		ID: "ctg_1", WorkspaceID: "wsp_1", ContentID: "ctn_1", VersionID: "ctv_1", Model: store.ModelTree,
		PayloadRef: string(staged.ManifestRef), DeleteAfter: now,
	}))
	worker, err := NewScopedTreeGCWorker(contentStore, tree, GCBindingScope{WorkspaceID: "wsp_1", Domain: "test_tree", BindingKey: "fixture_1"}, "worker-1", time.Minute)
	require.NoError(t, err)
	worker.now = func() time.Time { return now }

	result, err := worker.RunOnce(ctx, 10)
	require.NoError(t, err)
	require.Equal(t, GCWorkerResult{Leased: 1, Retained: 1}, result)
	manifest, err := tree.OpenManifest(ctx, staged.ManifestRef)
	require.NoError(t, err)
	require.Equal(t, "ctv_1", manifest.VersionID)
}

func TestScopedGCWorkerDoesNotLeaseOtherBinding(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.VersionedContent{}, &store.ContentVersion{}, &store.StagedPayloadGC{}))
	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	now := time.Date(2026, 8, 11, 10, 0, 0, 0, time.UTC)
	for _, binding := range []string{"fixture_1", "fixture_2"} {
		contentID := "ctn_" + binding
		require.NoError(t, db.Create(&store.VersionedContent{ID: contentID, WorkspaceID: "wsp_1", Domain: "test_tree", BindingKey: binding, Model: store.ModelTree, MaxRetainedVersions: 1, MaxStoredPayloadBytes: 1, MaxPins: 1, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now}).Error)
		require.NoError(t, db.Create(&store.StagedPayloadGC{ID: "ctg_" + binding, WorkspaceID: "wsp_1", ContentID: contentID, VersionID: "ctv_" + binding, Model: store.ModelTree, PayloadRef: "wsp_1/" + contentID, Status: store.GCStatusPending, DeleteAfter: now, CreatedAt: now, UpdatedAt: now}).Error)
	}
	worker, err := NewScopedTreeGCWorker(store.New(db), tree, GCBindingScope{WorkspaceID: "wsp_1", Domain: "test_tree", BindingKey: "fixture_1"}, "worker-1", time.Minute)
	require.NoError(t, err)
	worker.now = func() time.Time { return now }
	result, err := worker.RunOnce(ctx, 10)
	require.NoError(t, err)
	require.Equal(t, 1, result.Leased)
	var other store.StagedPayloadGC
	require.NoError(t, db.First(&other, "id = ?", "ctg_fixture_2").Error)
	require.Equal(t, store.GCStatusPending, other.Status)
	require.Empty(t, other.LeaseOwner)
}

func TestTreeGCWorkerFailsClosedForLegacyDocumentRow(t *testing.T) {
	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })

	worker := &GCWorker{tree: tree}
	err = worker.deletePayload(context.Background(), store.StagedPayloadGC{
		Model: store.ModelDocument, PayloadRef: "legacy-document-ref",
	})
	require.ErrorIs(t, err, store.ErrModelConflict)
}
