package automation

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	contentstoreservice "github.com/tidbcloud/AgentTeam/internal/service/content"
	automationstore "github.com/tidbcloud/AgentTeam/internal/store/automation"
	contentstore "github.com/tidbcloud/AgentTeam/internal/store/content"
	"gorm.io/gorm"
)

type automationContentSourceFixture struct {
	db             *gorm.DB
	source         ContentStoreAutomationSource
	contentID      string
	initialVersion string
	initialSource  string
	initialDigest  string
}

func newAutomationContentSourceFixture(t *testing.T) automationContentSourceFixture {
	t.Helper()
	db, err := openServiceTestSQLiteDB(t)
	require.NoError(t, err)
	migrateAutomationServiceTestSchema(t, db,
		&automationstore.AutomationContentPublication{}, &automationstore.AutomationContentDefinition{},
		&contentstore.Authority{}, &contentstore.VersionedContent{}, &contentstore.ContentVersion{},
		&contentstore.VersionOperation{}, &contentstore.VersionPath{}, &contentstore.VersionPin{},
		&contentstore.Outbox{}, &contentstore.StagedPayloadGC{},
	)
	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	var sequence atomic.Int64
	contentDB := contentstore.New(db)
	content, err := contentstoreservice.NewTree(contentDB, tree, func(prefix string) (string, error) {
		return fmt.Sprintf("%s_%d", prefix, sequence.Add(1)), nil
	})
	require.NoError(t, err)
	ctx := context.Background()
	lineage, err := content.CreateContent(ctx, contentstoreservice.CreateContentRequest{
		WorkspaceID: "wsp_1", Domain: "automation", BindingKey: "team/repo", Model: contentstore.ModelTree, Actor: "usr_1",
	})
	require.NoError(t, err)
	initialSource := "name: Build\non: push\njobs:\n  build:\n    steps:\n      - run: echo old\n"
	initial, err := content.CommitVersion(ctx, contentstoreservice.CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: lineage.ID, ExpectedGeneration: lineage.Generation,
		Actor: "usr_1", Model: contentstore.ModelTree, TreeLimits: contentpayload.DefaultLimits,
		Files: []contentpayload.File{{Path: ".agentteam/build.yml", Size: int64(len(initialSource)), Content: bytes.NewBufferString(initialSource)}},
	})
	require.NoError(t, err)
	_, err = contentDB.ActivateNativeAuthority(ctx, contentstore.ActivateNativeAuthorityParams{
		WorkspaceID: "wsp_1", Domain: "automation", BindingKey: "team/repo", ExpectedGeneration: 0,
		Checkpoint: initial.Version.ID, Actor: "usr_1", Now: time.Now().UTC(),
	})
	require.NoError(t, err)
	initialDigest := sha256.Sum256([]byte(initialSource))
	automations := automationstore.New(db)
	require.NoError(t, automations.RecordContentPublication(ctx, automationstore.AutomationContentPublication{
		WorkspaceID: "wsp_1", ContentID: lineage.ID, ContentVersionID: initial.Version.ID,
		RepositoryOwner: "team", RepositoryName: "repo", Ref: "main", ChangedPathsJSON: `[".agentteam/build.yml"]`,
		ActorID: "usr_1", CreatedAt: time.Now().UTC(),
	}, []automationstore.AutomationContentDefinition{{
		DefinitionPath: ".agentteam/build.yml", Digest: hex.EncodeToString(initialDigest[:]), SizeBytes: int64(len(initialSource)),
	}}, "", automationstore.ContentPublicationRecordStrict))
	return automationContentSourceFixture{
		db: db,
		source: ContentStoreAutomationSource{
			Content: content, Authority: contentDB, Store: automations,
			WorkRoot: t.TempDir(), Limits: contentpayload.DefaultLimits,
		},
		contentID: lineage.ID, initialVersion: initial.Version.ID, initialSource: initialSource,
		initialDigest: hex.EncodeToString(initialDigest[:]),
	}
}

func TestContentStoreAutomationSourcePublishesExactVersions(t *testing.T) {
	fixture := newAutomationContentSourceFixture(t)
	source := fixture.source
	ctx := context.Background()

	loaded, err := source.GetAutomationFile(ctx, AutomationSourceRequest{WorkspaceID: "wsp_1", RepositoryOwner: "team", RepositoryName: "repo", DefinitionPath: ".agentteam/build.yml"})
	require.NoError(t, err)
	require.Equal(t, fixture.contentID, loaded.ContentID)
	require.Equal(t, fixture.initialVersion, loaded.ContentVersionID)
	require.Equal(t, fixture.initialSource, loaded.Content)

	updatedSource := "name: Build\non: push\njobs:\n  build:\n    steps:\n      - run: echo new\n"
	updated, err := source.PutAutomationFile(ctx, AutomationSourceWriteRequest{
		WorkspaceID: "wsp_1", Actor: "usr_1", RepositoryOwner: "team", RepositoryName: "repo",
		DefinitionPath: ".agentteam/build.yml", Branch: "main", Content: updatedSource, SHA: loaded.SHA,
	})
	require.NoError(t, err)
	require.Equal(t, fixture.contentID, updated.Content.ContentID)
	require.NotEqual(t, fixture.initialVersion, updated.Content.ContentVersionID)
	require.Equal(t, updated.Content.ContentVersionID, updated.Commit.ContentVersionID)
	historical, err := source.GetAutomationFile(ctx, AutomationSourceRequest{
		WorkspaceID: "wsp_1", RepositoryOwner: "team", RepositoryName: "repo",
		DefinitionPath: ".agentteam/build.yml", GitRef: fixture.initialVersion,
	})
	require.NoError(t, err)
	require.Equal(t, fixture.initialSource, historical.Content)
	current, err := source.GetAutomationFile(ctx, AutomationSourceRequest{
		WorkspaceID: "wsp_1", RepositoryOwner: "team", RepositoryName: "repo",
		DefinitionPath: ".agentteam/build.yml", GitRef: "main",
	})
	require.NoError(t, err)
	require.Equal(t, updatedSource, current.Content)

	files, err := source.ListAutomationFiles(ctx, AutomationSourceRequest{WorkspaceID: "wsp_1", RepositoryOwner: "team", RepositoryName: "repo"})
	require.NoError(t, err)
	require.Len(t, files, 1)
	require.Equal(t, updated.Content.ContentVersionID, files[0].ContentVersionID)

	require.NoError(t, source.DeleteAutomationFile(ctx, AutomationSourceDeleteRequest{
		WorkspaceID: "wsp_1", Actor: "usr_1", RepositoryOwner: "team", RepositoryName: "repo",
		DefinitionPath: ".agentteam/build.yml", Branch: "main", SHA: updated.Content.SHA,
	}))
	files, err = source.ListAutomationFiles(ctx, AutomationSourceRequest{WorkspaceID: "wsp_1", RepositoryOwner: "team", RepositoryName: "repo"})
	require.NoError(t, err)
	require.Empty(t, files)
}

func TestContentStoreAutomationSourceRecoversCommittedMutationFromOperationReceipt(t *testing.T) {
	fixture := newAutomationContentSourceFixture(t)
	ctx := context.Background()
	trigger := fmt.Sprintf(`CREATE TRIGGER fail_automation_publication BEFORE INSERT ON automation_content_publications
		WHEN NEW.content_version_id <> '%s' BEGIN SELECT RAISE(ABORT, 'projection blocked'); END`, fixture.initialVersion)
	require.NoError(t, fixture.db.Exec(trigger).Error)
	request := AutomationSourceWriteRequest{
		WorkspaceID: "wsp_1", Actor: "usr_1", RepositoryOwner: "team", RepositoryName: "repo",
		DefinitionPath: ".agentteam/build.yml", Branch: "main", Content: strings.Replace(fixture.initialSource, "old", "receipt", 1),
		SHA: fixture.initialDigest,
	}
	_, err := fixture.source.PutAutomationFile(ctx, request)
	require.ErrorContains(t, err, "projection blocked")
	content, err := fixture.source.Content.GetContent(ctx, "wsp_1", fixture.contentID)
	require.NoError(t, err)
	require.NotEqual(t, fixture.initialVersion, content.HeadVersionID)
	var versions int64
	require.NoError(t, fixture.db.Model(&contentstore.ContentVersion{}).Count(&versions).Error)
	require.Equal(t, int64(2), versions)

	require.NoError(t, fixture.db.Exec("DROP TRIGGER fail_automation_publication").Error)
	recovered, err := fixture.source.PutAutomationFile(ctx, request)
	require.NoError(t, err)
	require.Equal(t, content.HeadVersionID, recovered.Content.ContentVersionID)
	require.NoError(t, fixture.db.Model(&contentstore.ContentVersion{}).Count(&versions).Error)
	require.Equal(t, int64(2), versions, "receipt recovery must not create another content version")
	loaded, err := fixture.source.GetAutomationFile(ctx, AutomationSourceRequest{
		WorkspaceID: "wsp_1", RepositoryOwner: "team", RepositoryName: "repo", DefinitionPath: ".agentteam/build.yml",
	})
	require.NoError(t, err)
	require.Equal(t, request.Content, loaded.Content)
}

func TestContentStoreAutomationSourceRejectsStaleDigestAndHead(t *testing.T) {
	fixture := newAutomationContentSourceFixture(t)
	ctx := context.Background()
	request := AutomationSourceWriteRequest{
		WorkspaceID: "wsp_1", Actor: "usr_1", RepositoryOwner: "team", RepositoryName: "repo",
		DefinitionPath: ".agentteam/build.yml", Branch: "main", Content: fixture.initialSource,
	}
	request.SHA = strings.Repeat("0", 64)
	_, err := fixture.source.PutAutomationFile(ctx, request)
	require.ErrorIs(t, err, ErrConflict)

	unprojected, err := fixture.source.Content.CommitVersion(ctx, contentstoreservice.CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: fixture.contentID, ExpectedHeadVersionID: fixture.initialVersion,
		ExpectedGeneration: 1, Actor: "usr_1", Model: contentstore.ModelTree, TreeLimits: contentpayload.DefaultLimits,
		Files: []contentpayload.File{{Path: ".agentteam/build.yml", Size: int64(len(fixture.initialSource)), Content: bytes.NewBufferString(fixture.initialSource)}},
	})
	require.NoError(t, err)
	require.NotEqual(t, fixture.initialVersion, unprojected.Version.ID)
	request.SHA = fixture.initialDigest
	_, err = fixture.source.PutAutomationFile(ctx, request)
	require.ErrorIs(t, err, ErrConflict)
}

func TestContentStoreAutomationSourceRequiresNativeAuthority(t *testing.T) {
	fixture := newAutomationContentSourceFixture(t)
	require.NoError(t, fixture.db.Model(&contentstore.Authority{}).
		Where("workspace_id = ? AND domain = ? AND binding_key = ?", "wsp_1", "automation", "team/repo").
		Update("state", contentstore.AuthorityStateLegacyActive).Error)
	_, err := fixture.source.GetAutomationFile(context.Background(), AutomationSourceRequest{
		WorkspaceID: "wsp_1", RepositoryOwner: "team", RepositoryName: "repo", DefinitionPath: ".agentteam/build.yml",
	})
	require.ErrorContains(t, err, "binding is not native")
}

func TestCollectAutomationContentWorkspaceEnforcesBounds(t *testing.T) {
	t.Run("file bytes", func(t *testing.T) {
		root := t.TempDir()
		require.NoError(t, os.WriteFile(filepath.Join(root, "large.txt"), []byte("1234"), 0o600))
		limits := contentpayload.DefaultLimits
		limits.MaxFileBytes = 3
		_, _, err := collectAutomationContentWorkspace(root, limits, "wsp_1", "ctn_1")
		require.ErrorContains(t, err, "exceeds publication bounds")
	})
	t.Run("total bytes", func(t *testing.T) {
		root := t.TempDir()
		require.NoError(t, os.WriteFile(filepath.Join(root, "one.txt"), []byte("123"), 0o600))
		require.NoError(t, os.WriteFile(filepath.Join(root, "two.txt"), []byte("456"), 0o600))
		limits := contentpayload.DefaultLimits
		limits.MaxTotalBytes = 5
		_, _, err := collectAutomationContentWorkspace(root, limits, "wsp_1", "ctn_1")
		require.ErrorContains(t, err, "exceeds publication bounds")
	})
	t.Run("definition count", func(t *testing.T) {
		root := filepath.Join(t.TempDir(), ".agentteam")
		require.NoError(t, os.MkdirAll(root, 0o700))
		for index := 0; index <= automationstore.MaxAutomationDefinitionsPerVersion; index++ {
			require.NoError(t, os.WriteFile(filepath.Join(root, fmt.Sprintf("definition-%03d.yml", index)), []byte("name: bounded"), 0o600))
		}
		_, _, err := collectAutomationContentWorkspace(filepath.Dir(root), contentpayload.DefaultLimits, "wsp_1", "ctn_1")
		require.ErrorContains(t, err, "automation definitions exceed")
	})
}

type materializeCancelingContent struct {
	AutomationNativeContentStore
	cancel func()
}

func (content materializeCancelingContent) MaterializeTree(ctx context.Context, _, _, _, _ string, _ contentpayload.MaterializeLimits) error {
	content.cancel()
	<-ctx.Done()
	return ctx.Err()
}

func TestContentStoreAutomationSourceCleansMaterializationAfterCancellation(t *testing.T) {
	fixture := newAutomationContentSourceFixture(t)
	ctx, cancel := context.WithCancel(context.Background())
	workRoot := t.TempDir()
	fixture.source.Content = materializeCancelingContent{AutomationNativeContentStore: fixture.source.Content, cancel: cancel}
	fixture.source.WorkRoot = workRoot
	_, err := fixture.source.PutAutomationFile(ctx, AutomationSourceWriteRequest{
		WorkspaceID: "wsp_1", Actor: "usr_1", RepositoryOwner: "team", RepositoryName: "repo",
		DefinitionPath: ".agentteam/build.yml", Branch: "main", Content: fixture.initialSource, SHA: fixture.initialDigest,
	})
	require.True(t, errors.Is(err, context.Canceled))
	entries, readErr := os.ReadDir(workRoot)
	require.NoError(t, readErr)
	require.Empty(t, entries)
}

type corruptingAutomationContent struct{ AutomationNativeContentStore }

func (content corruptingAutomationContent) OpenFile(context.Context, string, string, string, string, *contentpayload.ByteRange) (io.ReadCloser, contentpayload.BlobMeta, error) {
	corrupt := strings.Repeat("x", len("name: Build\non: push\njobs:\n  build:\n    steps:\n      - run: echo old\n"))
	digest := sha256.Sum256([]byte("name: Build\non: push\njobs:\n  build:\n    steps:\n      - run: echo old\n"))
	return io.NopCloser(strings.NewReader(corrupt)), contentpayload.BlobMeta{Digest: contentpayload.Digest(hex.EncodeToString(digest[:])), SizeBytes: int64(len(corrupt))}, nil
}

func TestContentStoreAutomationSourceVerifiesProjectedDigest(t *testing.T) {
	fixture := newAutomationContentSourceFixture(t)
	fixture.source.Content = corruptingAutomationContent{AutomationNativeContentStore: fixture.source.Content}
	_, err := fixture.source.GetAutomationFile(context.Background(), AutomationSourceRequest{
		WorkspaceID: "wsp_1", RepositoryOwner: "team", RepositoryName: "repo", DefinitionPath: ".agentteam/build.yml",
	})
	require.ErrorContains(t, err, "projection mismatch")
}
