package automation

import (
	"bytes"
	"context"
	"errors"
	"os"
	"path/filepath"
	"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"
	resourceactivityservice "github.com/tidbcloud/AgentTeam/internal/service/resource/activity"
	automationstore "github.com/tidbcloud/AgentTeam/internal/store/automation"
	contentstore "github.com/tidbcloud/AgentTeam/internal/store/content"
	activitystore "github.com/tidbcloud/AgentTeam/internal/store/resource/activity"
	"gorm.io/gorm"
)

type routedAutomationRunStore struct{ run automationstore.AutomationRun }

func (store routedAutomationRunStore) GetRun(context.Context, string, string) (automationstore.AutomationRun, error) {
	return store.run, nil
}

type recordingWikiPublisher struct {
	calls int
	err   error
}

func (publisher *recordingWikiPublisher) PublishWikiPages(context.Context, runtime.WikiPublishRequest) (runtime.WikiPublishResult, error) {
	publisher.calls++
	return runtime.WikiPublishResult{}, publisher.err
}

type recordingTreePublisher struct {
	calls int
	err   error
}

func (publisher *recordingTreePublisher) PublishRepositoryTree(context.Context, runtime.TreePublishRequest) (runtime.TreePublishResult, error) {
	publisher.calls++
	return runtime.TreePublishResult{}, publisher.err
}

func TestAutomationPublishersRouteByRunProvenanceWithoutFallback(t *testing.T) {
	legacyWiki, nativeWiki := &recordingWikiPublisher{}, &recordingWikiPublisher{err: errors.New("native wiki unavailable")}
	wiki := AuthorityRoutedAutomationWikiPublisher{
		Runs:   routedAutomationRunStore{run: automationstore.AutomationRun{ContentID: "ctn_1", ContentVersionID: "ctv_1"}},
		Legacy: legacyWiki, Native: nativeWiki,
	}
	_, err := wiki.PublishWikiPages(t.Context(), runtime.WikiPublishRequest{WorkspaceID: "wsp_1", RunID: "run_1"})
	require.ErrorContains(t, err, "native wiki unavailable")
	require.Zero(t, legacyWiki.calls)
	require.Equal(t, 1, nativeWiki.calls)

	legacyTree, nativeTree := &recordingTreePublisher{}, &recordingTreePublisher{}
	tree := AuthorityRoutedAutomationTreePublisher{
		Runs: routedAutomationRunStore{run: automationstore.AutomationRun{}}, Legacy: legacyTree, Native: nativeTree,
	}
	_, err = tree.PublishRepositoryTree(t.Context(), runtime.TreePublishRequest{WorkspaceID: "wsp_1", RunID: "run_legacy"})
	require.NoError(t, err)
	require.Equal(t, 1, legacyTree.calls)
	require.Zero(t, nativeTree.calls)
}

func TestAutomationNativePublishOperationKeyIncludesPayloadBytes(t *testing.T) {
	service := NewAutomationsService(&automationstore.Store{}, nil, nil)
	paths := []string{"page.md"}
	firstFiles := []contentpayload.File{{Path: paths[0], Size: 4, Content: bytes.NewBufferString("aaaa")}}
	secondFiles := []contentpayload.File{{Path: paths[0], Size: 4, Content: bytes.NewBufferString("bbbb")}}
	first, _, err := automationNativePublishProjection(service, "run_1", "job_1", "team/repo", paths, firstFiles)
	require.NoError(t, err)
	second, _, err := automationNativePublishProjection(service, "run_1", "job_1", "team/repo", paths, secondFiles)
	require.NoError(t, err)
	require.NotEqual(t, first, second)
	require.Equal(t, "aaaa", readPayloadFile(t, firstFiles[0]))
}

func TestNativeAutomationPublishProjectsDefinitionsAndDrivesExactConsumers(t *testing.T) {
	fixture := newAutomationContentSourceFixture(t)
	require.NoError(t, fixture.db.AutoMigrate(&activitystore.ResourceActivityEvent{}, &activitystore.ResourceActivityLatestCommit{}))
	now := time.Date(2026, 8, 22, 10, 0, 0, 0, time.UTC)
	service := NewAutomationsService(fixture.source.Store, func() time.Time { return now }, nil)
	service.SetAuthorizer(AllowAutomationAuthorizer{})
	_, err := fixture.source.Store.CreateRun(t.Context(), automationstore.AutomationRun{
		ID: "run_publisher", WorkspaceID: "wsp_1", Name: "Publisher", Status: automationstore.AutomationRunStatusRunning,
		OwnerPrincipalID: "usr_1", CreatedByPrincipalID: "usr_1", CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)

	root := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(root, ".agentteam"), 0o755))
	definition := "name: Generated\non: push\njobs:\n  build:\n    steps:\n      - run: echo generated\n"
	require.NoError(t, os.WriteFile(filepath.Join(root, ".agentteam", "generated.yml"), []byte(definition), 0o600))
	require.NoError(t, os.WriteFile(filepath.Join(root, "artifact.txt"), []byte("artifact"), 0o600))
	publisher := ContentStoreAutomationPublisher{
		Content: fixture.source.Content, Authority: fixture.source.Authority, Automations: service,
		Limits: contentpayload.DefaultLimits, Now: func() time.Time { return now },
	}
	published, err := publisher.PublishRepositoryTree(t.Context(), runtime.TreePublishRequest{
		WorkspaceID: "wsp_1", RunID: "run_publisher", JobRunID: "job_publisher", Repository: "team/repo",
		SourcePath: root, DefaultBranch: "main",
	})
	require.NoError(t, err)

	publication, err := fixture.source.Store.GetContentPublication(t.Context(), "wsp_1", fixture.contentID, published.CommitSHA)
	require.NoError(t, err)
	require.Equal(t, `[".agentteam/generated.yml","artifact.txt"]`, publication.ChangedPathsJSON)
	definitions, err := fixture.source.Store.ListContentDefinitions(t.Context(), "wsp_1", fixture.contentID, published.CommitSHA, automationstore.MaxAutomationDefinitionsPerVersion)
	require.NoError(t, err)
	require.Len(t, definitions, 1)
	require.Equal(t, ".agentteam/generated.yml", definitions[0].DefinitionPath)

	var outbox contentstore.Outbox
	require.NoError(t, fixture.db.Where("version_id = ?", published.CommitSHA).First(&outbox).Error)
	event := contentstoreservice.VersionCommittedEvent{
		ID: outbox.ID, WorkspaceID: outbox.WorkspaceID, ContentID: outbox.ContentID, VersionID: outbox.VersionID,
	}
	automationConsumer, err := NewContentVersionAutomationConsumer(fixture.source.Content, service)
	require.NoError(t, err)
	activityConsumer, err := resourceactivityservice.NewContentVersionActivityConsumer(fixture.source.Content, activitystore.New(fixture.db))
	require.NoError(t, err)
	for range 2 {
		require.NoError(t, automationConsumer.ConsumeVersionCommitted(t.Context(), event))
		require.NoError(t, activityConsumer.ConsumeVersionCommitted(t.Context(), event))
	}
	var runCount int64
	require.NoError(t, fixture.db.Model(&automationstore.AutomationRun{}).
		Where("source_kind = ? AND content_version_id = ?", automationSourceKindContentVersion, published.CommitSHA).Count(&runCount).Error)
	require.Equal(t, int64(1), runCount)
	var activityCount int64
	require.NoError(t, fixture.db.Model(&activitystore.ResourceActivityEvent{}).
		Where("source = ? AND source_event_id = ?", activitystore.SourceContentStore, outbox.ID).Count(&activityCount).Error)
	require.Equal(t, int64(1), activityCount)

	secondRoot := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(secondRoot, ".agentteam"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(secondRoot, ".agentteam", "generated.yml"), []byte(definition), 0o600))
	require.NoError(t, os.WriteFile(filepath.Join(secondRoot, "artifact.txt"), []byte("artifact-v3"), 0o600))
	second, err := publisher.PublishRepositoryTree(t.Context(), runtime.TreePublishRequest{
		WorkspaceID: "wsp_1", RunID: "run_publisher", JobRunID: "job_publisher_v3", Repository: "team/repo",
		SourcePath: secondRoot, DefaultBranch: "main",
	})
	require.NoError(t, err)
	require.NotEqual(t, published.CommitSHA, second.CommitSHA)
	replayed, err := publisher.PublishRepositoryTree(t.Context(), runtime.TreePublishRequest{
		WorkspaceID: "wsp_1", RunID: "run_publisher", JobRunID: "job_publisher", Repository: "team/repo",
		SourcePath: root, DefaultBranch: "main",
	})
	require.NoError(t, err)
	require.Equal(t, published.CommitSHA, replayed.CommitSHA)
	current, err := fixture.source.Store.GetCurrentContentRepository(t.Context(), "wsp_1", "team", "repo")
	require.NoError(t, err)
	require.Equal(t, second.CommitSHA, current.ContentVersionID)
}

func TestNativeAutomationPublishRecoversCrashBeforeProjectionAfterLaterCurrent(t *testing.T) {
	fixture := newAutomationContentSourceFixture(t)
	require.NoError(t, fixture.db.AutoMigrate(&activitystore.ResourceActivityEvent{}, &activitystore.ResourceActivityLatestCommit{}))
	now := time.Date(2026, 8, 22, 10, 0, 0, 0, time.UTC)
	service := NewAutomationsService(fixture.source.Store, func() time.Time { return now }, nil)
	service.SetAuthorizer(AllowAutomationAuthorizer{})
	_, err := fixture.source.Store.CreateRun(t.Context(), automationstore.AutomationRun{
		ID: "run_crash_recovery_publisher", WorkspaceID: "wsp_1", Name: "Publisher",
		Status: automationstore.AutomationRunStatusRunning, OwnerPrincipalID: "usr_1",
		CreatedByPrincipalID: "usr_1", CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)
	publisher := ContentStoreAutomationPublisher{
		Content: fixture.source.Content, Authority: fixture.source.Authority, Automations: service,
		Limits: contentpayload.DefaultLimits, Now: func() time.Time { return now },
	}
	definition := "name: Recovered\non: push\njobs:\n  build:\n    steps:\n      - run: echo recovered\n"
	paths := []string{".agentteam/recovered.yml", "artifact.txt"}
	files := func(artifact string) []contentpayload.File {
		return []contentpayload.File{
			{Path: paths[0], Size: int64(len(definition)), Content: bytes.NewBufferString(definition)},
			{Path: paths[1], Size: int64(len(artifact)), Content: bytes.NewBufferString(artifact)},
		}
	}
	lineage, err := fixture.source.Content.GetContent(t.Context(), "wsp_1", fixture.contentID)
	require.NoError(t, err)
	v2Files := files("artifact-v2")
	v2Operation, _, err := automationNativePublishProjection(service, "run_crash_recovery_publisher", "job_v2", "team/repo", paths, v2Files)
	require.NoError(t, err)
	v2, err := fixture.source.Content.CommitVersion(t.Context(), contentstoreservice.CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: fixture.contentID, ExpectedHeadVersionID: fixture.initialVersion,
		ExpectedGeneration: lineage.Generation, Actor: "usr_1", Model: contentstore.ModelTree,
		Files: v2Files, TreeLimits: contentpayload.DefaultLimits, OperationKey: v2Operation,
		OperationNamespace: "automation-publish", OperationVersion: 1,
	})
	require.NoError(t, err)
	_, err = fixture.source.Store.GetContentPublication(t.Context(), "wsp_1", fixture.contentID, v2.Version.ID)
	require.ErrorIs(t, err, gorm.ErrRecordNotFound, "the crash window must leave only the durable Content Store receipt")

	v3Files := files("artifact-v3")
	v3Operation, v3Definitions, err := automationNativePublishProjection(service, "run_crash_recovery_publisher", "job_v3", "team/repo", paths, v3Files)
	require.NoError(t, err)
	v3, err := fixture.source.Content.CommitVersion(t.Context(), contentstoreservice.CommitVersionRequest{
		WorkspaceID: "wsp_1", ContentID: fixture.contentID, ExpectedHeadVersionID: v2.Version.ID,
		ExpectedGeneration: lineage.Generation, Actor: "usr_1", Model: contentstore.ModelTree,
		Files: v3Files, TreeLimits: contentpayload.DefaultLimits, OperationKey: v3Operation,
		OperationNamespace: "automation-publish", OperationVersion: 1,
	})
	require.NoError(t, err)
	require.NoError(t, fixture.source.Store.RecordContentPublication(t.Context(), automationstore.AutomationContentPublication{
		WorkspaceID: "wsp_1", ContentID: fixture.contentID, ContentVersionID: v3.Version.ID,
		RepositoryOwner: "team", RepositoryName: "repo", Ref: "main",
		ChangedPathsJSON: `[".agentteam/recovered.yml","artifact.txt"]`, ActorID: "usr_1", CreatedAt: now.Add(time.Minute),
	}, v3Definitions, fixture.initialVersion, automationstore.ContentPublicationRecordStrict))

	recoveredVersion, err := publisher.publish(t.Context(), "wsp_1", "run_crash_recovery_publisher", "job_v2", "team/repo", "main", files("artifact-v2"), paths)
	require.NoError(t, err)
	require.Equal(t, v2.Version.ID, recoveredVersion)
	current, err := fixture.source.Store.GetCurrentContentRepository(t.Context(), "wsp_1", "team", "repo")
	require.NoError(t, err)
	require.Equal(t, v3.Version.ID, current.ContentVersionID)
	definitions, err := fixture.source.Store.ListContentDefinitions(t.Context(), "wsp_1", fixture.contentID, v2.Version.ID, automationstore.MaxAutomationDefinitionsPerVersion)
	require.NoError(t, err)
	require.Len(t, definitions, 1)
	require.Equal(t, ".agentteam/recovered.yml", definitions[0].DefinitionPath)

	var outbox contentstore.Outbox
	require.NoError(t, fixture.db.Where("version_id = ?", v2.Version.ID).First(&outbox).Error)
	event := contentstoreservice.VersionCommittedEvent{
		ID: outbox.ID, WorkspaceID: "wsp_1", ContentID: fixture.contentID, VersionID: v2.Version.ID,
	}
	automationConsumer, err := NewContentVersionAutomationConsumer(fixture.source.Content, service)
	require.NoError(t, err)
	activityConsumer, err := resourceactivityservice.NewContentVersionActivityConsumer(fixture.source.Content, activitystore.New(fixture.db))
	require.NoError(t, err)
	for range 2 {
		require.NoError(t, automationConsumer.ConsumeVersionCommitted(t.Context(), event))
		require.NoError(t, activityConsumer.ConsumeVersionCommitted(t.Context(), event))
	}
	var runs int64
	require.NoError(t, fixture.db.Model(&automationstore.AutomationRun{}).
		Where("source_kind = ? AND content_version_id = ?", automationSourceKindContentVersion, v2.Version.ID).Count(&runs).Error)
	require.Equal(t, int64(1), runs)
	var activities int64
	require.NoError(t, fixture.db.Model(&activitystore.ResourceActivityEvent{}).
		Where("source = ? AND source_event_id = ?", activitystore.SourceContentStore, outbox.ID).Count(&activities).Error)
	require.Equal(t, int64(1), activities)
	current, err = fixture.source.Store.GetCurrentContentRepository(t.Context(), "wsp_1", "team", "repo")
	require.NoError(t, err)
	require.Equal(t, v3.Version.ID, current.ContentVersionID)
}

func readPayloadFile(t *testing.T, file contentpayload.File) string {
	t.Helper()
	var body bytes.Buffer
	_, err := body.ReadFrom(file.Content)
	require.NoError(t, err)
	return body.String()
}
