package automation

import (
	"bytes"
	"context"
	"fmt"
	"path/filepath"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	"github.com/tidbcloud/AgentTeam/internal/service/automation/runtime"
	contentstoreservice "github.com/tidbcloud/AgentTeam/internal/service/content"
	contentstorestore "github.com/tidbcloud/AgentTeam/internal/store/content"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

type mutableAutomationWorkspaceAccess struct{ allowed atomic.Bool }

func (access *mutableAutomationWorkspaceAccess) HasWorkspaceAccess(context.Context, string, string) (bool, error) {
	return access.allowed.Load(), nil
}

type recordingAutomationContentTreeReader struct {
	version contentstoreservice.Version
	limits  contentpayload.MaterializeLimits
	target  string
}

func (reader *recordingAutomationContentTreeReader) GetVersion(context.Context, string, string, string) (contentstoreservice.Version, error) {
	return reader.version, nil
}

func (reader *recordingAutomationContentTreeReader) MaterializeTree(_ context.Context, _, _, _, target string, limits contentpayload.MaterializeLimits) error {
	reader.target, reader.limits = target, limits
	return nil
}

func TestContentStoreAutomationCheckoutMaterializesExactBoundedVersion(t *testing.T) {
	reader := &recordingAutomationContentTreeReader{version: contentstoreservice.Version{ID: "ctv_1", ContentID: "ctn_1", FileCount: 2, SizeBytes: 12}}
	root, err := filepath.EvalSymlinks(t.TempDir())
	require.NoError(t, err)
	result, err := (ContentStoreAutomationCheckoutMaterializer{Content: reader, MaxFileBytes: 8}).Materialize(t.Context(), runtime.CheckoutRequest{
		WorkspaceID: "wsp_1", ContentID: "ctn_1", ContentVersionID: "ctv_1",
		Destination: filepath.Join(root, "checkout"), Clean: true, MaxFiles: 3, MaxBytes: 20,
	})
	require.NoError(t, err)
	require.Equal(t, "content_version", result.Strategy)
	require.Equal(t, 2, result.Files)
	require.Equal(t, int64(12), result.Bytes)
	require.NotEmpty(t, reader.target)
	require.Equal(t, contentpayload.MaterializeLimits{MaxFiles: 3, MaxFileBytes: 8, MaxTotalBytes: 20}, reader.limits)
}

func TestContentStoreAutomationCheckoutRejectsVersionOutsideRequestBounds(t *testing.T) {
	reader := &recordingAutomationContentTreeReader{version: contentstoreservice.Version{ID: "ctv_1", ContentID: "ctn_1", FileCount: 4, SizeBytes: 12}}
	root, err := filepath.EvalSymlinks(t.TempDir())
	require.NoError(t, err)
	_, err = (ContentStoreAutomationCheckoutMaterializer{Content: reader}).Materialize(t.Context(), runtime.CheckoutRequest{
		WorkspaceID: "wsp_1", ContentID: "ctn_1", ContentVersionID: "ctv_1",
		Destination: filepath.Join(root, "checkout"), MaxFiles: 3, MaxBytes: 20,
	})
	require.ErrorContains(t, err, "exceeds checkout bounds")
	require.Empty(t, reader.target)
}

func TestContentStoreAutomationCheckoutRevalidatesLiveAccess(t *testing.T) {
	db, err := openServiceTestSQLiteDB(t)
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&contentstorestore.Authority{}, &contentstorestore.VersionedContent{}, &contentstorestore.ContentVersion{},
		&contentstorestore.VersionOperation{}, &contentstorestore.VersionPath{}, &contentstorestore.VersionPin{},
		&contentstorestore.Outbox{}, &contentstorestore.StagedPayloadGC{}, &repos.ResourceRepository{},
	))
	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	var ids atomic.Int64
	contentDB := contentstorestore.New(db)
	core, err := contentstoreservice.NewTree(contentDB, tree, func(prefix string) (string, error) {
		return fmt.Sprintf("%s_%d", prefix, ids.Add(1)), nil
	})
	require.NoError(t, err)
	lineage, err := core.CreateContent(t.Context(), contentstoreservice.CreateContentRequest{
		WorkspaceID: "wsp_1", Domain: "automation", BindingKey: "team/repo", Model: contentstorestore.ModelTree, Actor: "usr_1",
	})
	require.NoError(t, err)
	committed, err := core.CommitVersion(t.Context(), contentstoreservice.CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: lineage.ID, ExpectedGeneration: 1, Actor: "usr_1", Model: contentstorestore.ModelTree,
		Files: []contentpayload.File{{Path: ".agentteam/build.yml", Size: 2, Content: bytes.NewBufferString("ok")}}, TreeLimits: contentpayload.DefaultLimits,
	})
	require.NoError(t, err)
	now := time.Now().UTC()
	require.NoError(t, db.Create(&repos.ResourceRepository{
		ID: "rrp_1", WorkspaceID: "wsp_1", ResourceType: repos.ResourceTypeAutomationRepository,
		ResourceID: repos.AutomationRepositoryResourceID("team/repo"), RepoRole: repos.RepoRolePrimary,
		RepoOwner: "team", RepoName: "repo", RepoFullName: "team/repo", CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now,
	}).Error)
	_, err = contentDB.ActivateNativeAuthority(t.Context(), contentstorestore.ActivateNativeAuthorityParams{
		WorkspaceID: "wsp_1", Domain: "automation", BindingKey: "team/repo", ExpectedGeneration: 0,
		Checkpoint: committed.Version.ID, Actor: "usr_1", Now: now,
	})
	require.NoError(t, err)
	workspace := &mutableAutomationWorkspaceAccess{}
	workspace.allowed.Store(true)
	policy, err := NewAutomationContentAccessPolicy(workspace, repos.New(db), contentDB)
	require.NoError(t, err)
	authorized, err := contentstoreservice.NewAuthorizedService(core, policy)
	require.NoError(t, err)
	metadataCtx := WithAutomationContentAccess(t.Context(), "wsp_1", "team/repo", "", 0, "usr_1", contentstoreservice.AccessReadMetadata)
	resolved, found, err := authorized.FindContentByBinding(metadataCtx, "wsp_1", "automation", "team/repo")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, lineage.ID, resolved.ID)
	materializer := ContentStoreAutomationCheckoutMaterializer{Content: authorized}
	checkoutRoot, err := filepath.EvalSymlinks(t.TempDir())
	require.NoError(t, err)
	request := runtime.CheckoutRequest{
		WorkspaceID: "wsp_1", ContentID: lineage.ID, ContentVersionID: committed.Version.ID, PrincipalID: "usr_1",
		Destination: filepath.Join(checkoutRoot, "allowed"), Clean: true, MaxFiles: 10, MaxBytes: 1024,
	}
	_, err = materializer.Materialize(t.Context(), request)
	require.NoError(t, err)

	workspace.allowed.Store(false)
	request.Destination = filepath.Join(checkoutRoot, "revoked")
	_, err = materializer.Materialize(t.Context(), request)
	require.ErrorIs(t, err, contentstoreservice.ErrAccessDenied)

	workspace.allowed.Store(true)
	require.NoError(t, db.Model(&repos.ResourceRepository{}).Where("id = ?", "rrp_1").Update("repo_full_name", "team/revoked").Error)
	request.Destination = filepath.Join(checkoutRoot, "revoked-binding")
	_, err = materializer.Materialize(t.Context(), request)
	require.ErrorIs(t, err, contentstoreservice.ErrAccessDenied)
	require.NoError(t, db.Model(&repos.ResourceRepository{}).Where("id = ?", "rrp_1").Update("repo_full_name", "team/repo").Error)

	var authority contentstorestore.Authority
	require.NoError(t, db.Where("workspace_id = ? AND domain = ? AND binding_key = ?", "wsp_1", "automation", "team/repo").First(&authority).Error)
	require.NoError(t, db.Model(&contentstorestore.Authority{}).Where("id = ?", authority.ID).Update("generation", 2).Error)
	request.Destination = filepath.Join(checkoutRoot, "stale-generation")
	_, err = materializer.Materialize(t.Context(), request)
	require.ErrorIs(t, err, contentstoreservice.ErrAccessDenied)
}
