package automation

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestStartRepositoryAutomationRunsForPushEventCreatesMatchingRuns(t *testing.T) {
	ctx := context.Background()
	store := newAutomationsServiceStoreForSourceTest(t)
	now := time.Date(2026, 6, 13, 10, 0, 0, 0, time.UTC)
	after := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
	source := newPushAutomationSourceControl(after, map[string]string{
		".agentteam/build.yml":  "name: Build\non:\n  push:\n    branches: [main]\n    paths: [cmd/**]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo build\n",
		".agentteam/docs.yml":   "name: Docs\non: [push]\njobs:\n  docs:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo docs\n",
		".agentteam/manual.yml": "name: Manual\non:\n  manual: {}\njobs:\n  manual:\n    steps:\n      - run: echo manual\n",
	})
	service := NewAutomationsService(store, func() time.Time { return now }, source)

	runs, err := service.StartRepositoryAutomationRunsForPushEvent(ctx, "ws-1", "automation-eventer", AutomationRepositoryPushEventMutation{
		RepositoryOwner:  "pingcap",
		RepositoryName:   "agentteam",
		DeliveryKey:      "github:installation:delivery-1",
		Ref:              "refs/heads/main",
		After:            after,
		ChangedPaths:     []string{"./cmd/server/main.go", "/README.md", "cmd/server/main.go"},
		EventPayloadJSON: `{"sender":"octocat"}`,
	})
	require.NoError(t, err)
	require.Len(t, runs, 2)
	require.Equal(t, []string{".agentteam/build.yml", ".agentteam/docs.yml"}, []string{runs[0].DefinitionPath, runs[1].DefinitionPath})
	require.Len(t, source.listRequests, 1)
	require.Equal(t, after, source.listRequests[0].GitRef)
	require.Len(t, source.resolveRequests, 3)
	for _, request := range source.resolveRequests {
		require.Equal(t, after, request.GitRef)
	}
	for _, run := range runs {
		require.Equal(t, "agent_git_service_repo", run.SourceKind)
		require.Equal(t, "pingcap", run.RepositoryOwner)
		require.Equal(t, "agentteam", run.RepositoryName)
		require.Equal(t, after, run.GitRef)
		require.Equal(t, after, run.CommitSHA)
		require.Equal(t, "push", run.TriggerKind)
		require.Equal(t, "github:installation:delivery-1", run.TriggerRef)
		require.Equal(t, "repository_ref", run.TargetKind)
		require.Equal(t, "pingcap/agentteam@refs/heads/main#"+after, run.TargetRef)
		require.JSONEq(t, `{"trigger_context":{"repository_full_name":"pingcap/agentteam","ref":"refs/heads/main","branch":"main","before_sha":"","after_sha":"`+after+`","event_id":"github:installation:delivery-1","trigger_kind":"push"}}`, run.Metadata)
		require.Equal(t, AutomationEventerPrincipalID, run.OwnerPrincipalID)
		require.Equal(t, AutomationEventerPrincipalID, run.CreatedByPrincipalID)
		require.NotNil(t, run.IdempotencyKey)
		require.True(t, strings.HasPrefix(*run.IdempotencyKey, "push:"), *run.IdempotencyKey)
		jobRuns, err := store.ListJobRuns(ctx, "ws-1", run.ID)
		require.NoError(t, err)
		require.Len(t, jobRuns, 1)
		require.JSONEq(t, `"ubuntu-latest"`, jobRuns[0].RunsOnJSON)
		queueJobs, err := store.ListJobsForRun(ctx, "ws-1", run.ID)
		require.NoError(t, err)
		require.Len(t, queueJobs, 1)
		require.JSONEq(t, `["ubuntu-latest"]`, queueJobs[0].LabelsJSON)
	}
}

func TestStartRepositoryAutomationRunsForPushEventIsIdempotentByDefinitionPath(t *testing.T) {
	ctx := context.Background()
	store := newAutomationsServiceStoreForSourceTest(t)
	now := time.Date(2026, 6, 13, 10, 30, 0, 0, time.UTC)
	after := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
	source := newPushAutomationSourceControl(after, map[string]string{
		".agentteam/build.yml": "name: Build\non: push\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo build\n",
	})
	service := NewAutomationsService(store, func() time.Time { return now }, source)

	input := AutomationRepositoryPushEventMutation{
		RepositoryOwner:  "pingcap",
		RepositoryName:   "agentteam",
		DeliveryKey:      "github:installation:delivery-2",
		Ref:              "refs/heads/main",
		After:            after,
		ChangedPaths:     []string{"cmd/server/main.go"},
		EventPayloadJSON: `{"delivery":"2"}`,
	}
	first, err := service.StartRepositoryAutomationRunsForPushEvent(ctx, "ws-1", "automation-eventer", input)
	require.NoError(t, err)
	require.Len(t, first, 1)
	second, err := service.StartRepositoryAutomationRunsForPushEvent(ctx, "ws-1", "automation-eventer", input)
	require.NoError(t, err)
	require.Len(t, second, 1)
	require.Equal(t, first[0].ID, second[0].ID)
	require.Len(t, source.listRequests, 2)
	require.Len(t, source.resolveRequests, 1)

	jobRuns, err := store.ListJobRuns(ctx, "ws-1", first[0].ID)
	require.NoError(t, err)
	require.Len(t, jobRuns, 1)
	metrics, err := store.QueueMetrics(ctx, "ws-1", now)
	require.NoError(t, err)
	require.Equal(t, int64(1), metrics.Ready)
}

func TestStartRepositoryAutomationRunsForPushEventRejectsIdempotencyReplayMismatch(t *testing.T) {
	ctx := context.Background()
	store := newAutomationsServiceStoreForSourceTest(t)
	now := time.Date(2026, 6, 13, 11, 0, 0, 0, time.UTC)
	after := "cccccccccccccccccccccccccccccccccccccccc"
	source := newPushAutomationSourceControl(after, map[string]string{
		".agentteam/build.yml": "name: Build\non: push\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo build\n",
	})
	service := NewAutomationsService(store, func() time.Time { return now }, source)

	input := AutomationRepositoryPushEventMutation{
		RepositoryOwner:  "pingcap",
		RepositoryName:   "agentteam",
		DeliveryKey:      "github:installation:delivery-3",
		Ref:              "refs/heads/main",
		After:            after,
		ChangedPaths:     []string{"cmd/server/main.go"},
		EventPayloadJSON: `{"delivery":"3"}`,
	}
	first, err := service.StartRepositoryAutomationRunsForPushEvent(ctx, "ws-1", "automation-eventer", input)
	require.NoError(t, err)
	require.Len(t, first, 1)

	mismatched := input
	mismatched.EventPayloadJSON = `{"delivery":"3","retry":true}`
	_, err = service.StartRepositoryAutomationRunsForPushEvent(ctx, "ws-1", "automation-eventer", mismatched)
	require.ErrorIs(t, err, ErrConflict)
	require.Contains(t, err.Error(), "idempotency key already belongs")
	require.Len(t, source.resolveRequests, 1)
}

func TestStartRepositoryAutomationRunsForPushEventDeleteRefNoops(t *testing.T) {
	tests := []struct {
		name  string
		after string
	}{
		{name: "empty after", after: ""},
		{name: "zero after", after: "0000000000000000000000000000000000000000"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			store := newAutomationsServiceStoreForSourceTest(t)
			now := time.Date(2026, 6, 13, 11, 30, 0, 0, time.UTC)
			source := newPushAutomationSourceControl("dddddddddddddddddddddddddddddddddddddddd", map[string]string{
				".agentteam/build.yml": "name: Build\non: push\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo build\n",
			})
			service := NewAutomationsService(store, func() time.Time { return now }, source)

			runs, err := service.StartRepositoryAutomationRunsForPushEvent(ctx, "ws-1", "automation-eventer", AutomationRepositoryPushEventMutation{
				RepositoryOwner: "pingcap",
				RepositoryName:  "agentteam",
				DeliveryKey:     "github:installation:delivery-4",
				Ref:             "refs/heads/main",
				After:           tt.after,
			})
			require.NoError(t, err)
			require.Empty(t, runs)
			require.Empty(t, source.listRequests)
			require.Empty(t, source.resolveRequests)
		})
	}
}

func TestStartRepositoryAutomationRunsForPushEventRejectsMutableAfter(t *testing.T) {
	ctx := context.Background()
	store := newAutomationsServiceStoreForSourceTest(t)
	now := time.Date(2026, 6, 13, 11, 45, 0, 0, time.UTC)
	source := newPushAutomationSourceControl("dddddddddddddddddddddddddddddddddddddddd", map[string]string{
		".agentteam/build.yml": "name: Build\non: push\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo build\n",
	})
	service := NewAutomationsService(store, func() time.Time { return now }, source)

	_, err := service.StartRepositoryAutomationRunsForPushEvent(ctx, "ws-1", "automation-eventer", AutomationRepositoryPushEventMutation{
		RepositoryOwner: "pingcap",
		RepositoryName:  "agentteam",
		DeliveryKey:     "github:installation:delivery-4",
		Ref:             "refs/heads/main",
		After:           "main",
	})
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.Contains(t, err.Error(), "after must be an immutable commit SHA")
	require.Empty(t, source.listRequests)
	require.Empty(t, source.resolveRequests)
}

func TestStartRepositoryAutomationRunsForPushEventUnknownPathsSkipPathFilteredAutomations(t *testing.T) {
	ctx := context.Background()
	store := newAutomationsServiceStoreForSourceTest(t)
	now := time.Date(2026, 6, 13, 12, 0, 0, 0, time.UTC)
	after := "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
	source := newPushAutomationSourceControl(after, map[string]string{
		".agentteam/all.yml":   "name: All\non: push\njobs:\n  all:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo all\n",
		".agentteam/paths.yml": "name: Paths\non:\n  push:\n    paths: [cmd/**]\njobs:\n  paths:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo paths\n",
	})
	service := NewAutomationsService(store, func() time.Time { return now }, source)

	runs, err := service.StartRepositoryAutomationRunsForPushEvent(ctx, "ws-1", "automation-eventer", AutomationRepositoryPushEventMutation{
		RepositoryOwner: "pingcap",
		RepositoryName:  "agentteam",
		DeliveryKey:     "github:installation:delivery-5",
		Ref:             "refs/heads/main",
		After:           after,
	})
	require.NoError(t, err)
	require.Len(t, runs, 1)
	require.Equal(t, ".agentteam/all.yml", runs[0].DefinitionPath)
}

func TestStartRepositoryAutomationRunsForPushEventAcceptsGenericEventIdentityAndActor(t *testing.T) {
	ctx := context.Background()
	store := newAutomationsServiceStoreForSourceTest(t)
	now := time.Date(2026, 6, 13, 12, 15, 0, 0, time.UTC)
	after := "ffffffffffffffffffffffffffffffffffffffff"
	source := newPushAutomationSourceControl(after, map[string]string{
		".agentteam/build.yml": "name: Build\non: push\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo build\n",
	})
	service := NewAutomationsService(store, func() time.Time { return now }, source)

	runs, err := service.StartRepositoryAutomationRunsForPushEvent(ctx, "ws-1", "automation-eventer", AutomationRepositoryPushEventMutation{
		RepositoryOwner: "pingcap",
		RepositoryName:  "agentteam",
		EventID:         "ags-local-merge-1",
		Ref:             "refs/heads/main",
		After:           after,
		ChangedPaths:    []string{"cmd/server/main.go"},
		ActorRef:        "user:alice",
		ActorDisplay:    "Alice",
	})
	require.NoError(t, err)
	require.Len(t, runs, 1)
	require.Equal(t, "ags-local-merge-1", runs[0].TriggerRef)
	triggerContext := ParseAutomationRunTriggerContext(runs[0].Metadata)
	require.NotNil(t, triggerContext)
	require.Equal(t, "ags-local-merge-1", triggerContext.EventID)
	require.Equal(t, "user:alice", triggerContext.ActorRef)
	require.Equal(t, "Alice", triggerContext.ActorDisplay)
	require.NotNil(t, runs[0].IdempotencyKey)
	require.True(t, strings.HasPrefix(*runs[0].IdempotencyKey, "push:"), *runs[0].IdempotencyKey)
}

type pushAutomationSourceControl struct {
	commitSHA       string
	contents        map[string]string
	files           []AutomationSourceFile
	listRequests    []AutomationSourceRequest
	resolveRequests []AutomationSourceRequest
}

func newPushAutomationSourceControl(commitSHA string, contents map[string]string) *pushAutomationSourceControl {
	files := make([]AutomationSourceFile, 0, len(contents))
	for definitionPath, content := range contents {
		files = append(files, AutomationSourceFile{RepositoryOwner: "pingcap", RepositoryName: "agentteam", Path: definitionPath, Name: automationSourceFileName(definitionPath), Size: int64(len([]byte(content))), Ref: commitSHA})
	}
	return &pushAutomationSourceControl{commitSHA: commitSHA, contents: contents, files: files}
}

func (c *pushAutomationSourceControl) ResolveAutomationSource(_ context.Context, request AutomationSourceRequest) (AutomationSourceSnapshot, error) {
	c.resolveRequests = append(c.resolveRequests, request)
	content, ok := c.contents[normalizeAutomationSourcePath(request.DefinitionPath)]
	if !ok {
		return AutomationSourceSnapshot{}, fmt.Errorf("missing automation content for %s", request.DefinitionPath)
	}
	commitSHA := c.commitSHA
	if commitSHA == "" {
		commitSHA = strings.TrimSpace(request.GitRef)
	}
	return AutomationSourceSnapshot{RepositoryOwner: strings.TrimSpace(request.RepositoryOwner), RepositoryName: strings.TrimSpace(request.RepositoryName), DefinitionPath: normalizeAutomationSourcePath(request.DefinitionPath), GitRef: strings.TrimSpace(request.GitRef), CommitSHA: commitSHA, Content: content}, nil
}

func (c *pushAutomationSourceControl) ListAutomationRepositories(context.Context, string, int, int) ([]AutomationSourceRepository, error) {
	return nil, nil
}

func (c *pushAutomationSourceControl) ListAutomationOrgRepositories(context.Context, string, string, int, int) ([]AutomationSourceRepository, error) {
	return nil, nil
}

func (c *pushAutomationSourceControl) ListAutomationFiles(_ context.Context, request AutomationSourceRequest) ([]AutomationSourceFile, error) {
	c.listRequests = append(c.listRequests, request)
	return append([]AutomationSourceFile(nil), c.files...), nil
}

func (c *pushAutomationSourceControl) GetAutomationFile(context.Context, AutomationSourceRequest) (AutomationSourceContent, error) {
	return AutomationSourceContent{}, nil
}

var _ AutomationSourceResolver = (*pushAutomationSourceControl)(nil)
var _ AutomationSourceControlPlane = (*pushAutomationSourceControl)(nil)
