package runmanage

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	workflowrunstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/run"
)

type repositoryStub struct {
	loadRun         func(context.Context, string, string) (workflowrunstore.Run, error)
	listRuns        func(context.Context, workflowrunstore.ListRunsParams) ([]workflowrunstore.Run, error)
	listActivations func(context.Context, workflowrunstore.ListChildrenParams) ([]workflowrunstore.Activation, error)
	listEffects     func(context.Context, workflowrunstore.ListChildrenParams) ([]workflowrunstore.Effect, error)
	listEvents      func(context.Context, workflowrunstore.ListEventsParams) ([]workflowrunstore.Event, error)
	loadEvent       func(context.Context, string, string, int64) (workflowrunstore.Event, error)
	cancelRun       func(context.Context, workflowrunstore.CancelRunParams) (workflowrunstore.Run, *workflowrunstore.Event, error)
}

func (stub repositoryStub) LoadRun(ctx context.Context, workspaceID, runID string) (workflowrunstore.Run, error) {
	if stub.loadRun == nil {
		return workflowrunstore.Run{}, fmt.Errorf("unexpected LoadRun")
	}
	return stub.loadRun(ctx, workspaceID, runID)
}

func (stub repositoryStub) ListRuns(ctx context.Context, params workflowrunstore.ListRunsParams) ([]workflowrunstore.Run, error) {
	if stub.listRuns == nil {
		return nil, fmt.Errorf("unexpected ListRuns")
	}
	return stub.listRuns(ctx, params)
}

func (stub repositoryStub) ListActivations(ctx context.Context, params workflowrunstore.ListChildrenParams) ([]workflowrunstore.Activation, error) {
	if stub.listActivations == nil {
		return nil, fmt.Errorf("unexpected ListActivations")
	}
	return stub.listActivations(ctx, params)
}

func (stub repositoryStub) ListEffects(ctx context.Context, params workflowrunstore.ListChildrenParams) ([]workflowrunstore.Effect, error) {
	if stub.listEffects == nil {
		return nil, fmt.Errorf("unexpected ListEffects")
	}
	return stub.listEffects(ctx, params)
}

func (stub repositoryStub) LoadEffectQueueContexts(context.Context, string, string) (map[string]workflowrunstore.EffectQueueContext, error) {
	return map[string]workflowrunstore.EffectQueueContext{}, nil
}

func (stub repositoryStub) ListEvents(ctx context.Context, params workflowrunstore.ListEventsParams) ([]workflowrunstore.Event, error) {
	if stub.listEvents == nil {
		return nil, fmt.Errorf("unexpected ListEvents")
	}
	return stub.listEvents(ctx, params)
}

func (stub repositoryStub) LoadEvent(ctx context.Context, workspaceID, runID string, sequence int64) (workflowrunstore.Event, error) {
	if stub.loadEvent == nil {
		return workflowrunstore.Event{}, fmt.Errorf("unexpected LoadEvent")
	}
	return stub.loadEvent(ctx, workspaceID, runID, sequence)
}

func (stub repositoryStub) CancelRun(ctx context.Context, params workflowrunstore.CancelRunParams) (workflowrunstore.Run, *workflowrunstore.Event, error) {
	if stub.cancelRun == nil {
		return workflowrunstore.Run{}, nil, fmt.Errorf("unexpected CancelRun")
	}
	return stub.cancelRun(ctx, params)
}

type fixedClock time.Time

func (clock fixedClock) Now() time.Time { return time.Time(clock) }

func TestListRunsUsesStatusScopedOpaqueCursor(t *testing.T) {
	now := time.Date(2026, 7, 21, 9, 10, 11, 0, time.UTC)
	first := runFixture("wri_03", workflowrunstore.RunStatusWaiting, now)
	second := runFixture("wri_02", workflowrunstore.RunStatusWaiting, now.Add(-time.Minute))
	third := runFixture("wri_01", workflowrunstore.RunStatusWaiting, now.Add(-2*time.Minute))
	first.StartedAt, second.StartedAt, third.StartedAt = timePtr(now), timePtr(now.Add(-time.Minute)), nil
	var calls []workflowrunstore.ListRunsParams
	service := New(repositoryStub{listRuns: func(_ context.Context, params workflowrunstore.ListRunsParams) ([]workflowrunstore.Run, error) {
		calls = append(calls, params)
		if params.BeforeID == "" {
			return []workflowrunstore.Run{first, second, third}, nil
		}
		require.Equal(t, second.ID, params.BeforeID)
		require.Equal(t, second.StartedAt, params.BeforeStartedAt)
		return []workflowrunstore.Run{third}, nil
	}})

	page, err := service.ListRuns(context.Background(), ListRunsInput{
		WorkspaceID: " wsp_1 ", PlanID: " pln_1 ", Status: " waiting ", Limit: 2,
	})
	require.NoError(t, err)
	require.Equal(t, []workflowrunstore.Run{first, second}, page.Items)
	require.NotEmpty(t, page.NextCursor)
	require.Equal(t, workflowrunstore.ListRunsParams{
		WorkspaceID: "wsp_1", PlanID: "pln_1", Status: workflowrunstore.RunStatusWaiting, Limit: 3,
	}, calls[0])

	next, err := service.ListRuns(context.Background(), ListRunsInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", Status: workflowrunstore.RunStatusWaiting, Limit: 2, Cursor: page.NextCursor,
	})
	require.NoError(t, err)
	require.Equal(t, []workflowrunstore.Run{third}, next.Items)
	require.Empty(t, next.NextCursor)

	_, err = service.ListRuns(context.Background(), ListRunsInput{
		WorkspaceID: "wsp_other", PlanID: "pln_1", Status: workflowrunstore.RunStatusWaiting, Limit: 2, Cursor: page.NextCursor,
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)
	_, err = service.ListRuns(context.Background(), ListRunsInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", Status: workflowrunstore.RunStatusRunning, Limit: 2, Cursor: page.NextCursor,
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)
	_, err = service.ListRuns(context.Background(), ListRunsInput{
		WorkspaceID: "wsp_1", PlanID: "pln_other", Status: workflowrunstore.RunStatusWaiting,
		Limit: 2, Cursor: page.NextCursor,
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)
	require.Len(t, calls, 2)
}

func TestListRunsRejectsRepositoryRunOutsidePlanFilter(t *testing.T) {
	now := time.Date(2026, 7, 21, 9, 10, 11, 0, time.UTC)
	outOfScope := runFixture("wri_1", workflowrunstore.RunStatusRunning, now)
	outOfScope.PlanID = "pln_other"
	service := New(repositoryStub{listRuns: func(_ context.Context, params workflowrunstore.ListRunsParams) ([]workflowrunstore.Run, error) {
		require.Equal(t, "pln_1", params.PlanID)
		return []workflowrunstore.Run{outOfScope}, nil
	}})

	_, err := service.ListRuns(context.Background(), ListRunsInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", Limit: 10,
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "out-of-scope")
}

func TestListRunsOrdersEqualAndUnstartedRunsByDescendingID(t *testing.T) {
	now := time.Date(2026, 7, 21, 9, 10, 11, 0, time.UTC)
	startedHigh := runFixture("wri_03", workflowrunstore.RunStatusRunning, now)
	startedLow := runFixture("wri_02", workflowrunstore.RunStatusRunning, now)
	unstartedHigh := runFixture("wri_05", workflowrunstore.RunStatusQueued, now)
	unstartedLow := runFixture("wri_04", workflowrunstore.RunStatusQueued, now)
	startedHigh.StartedAt, startedLow.StartedAt = timePtr(now), timePtr(now)

	service := New(repositoryStub{listRuns: func(_ context.Context, params workflowrunstore.ListRunsParams) ([]workflowrunstore.Run, error) {
		require.Equal(t, 5, params.Limit)
		return []workflowrunstore.Run{startedHigh, startedLow, unstartedHigh, unstartedLow}, nil
	}})
	page, err := service.ListRuns(context.Background(), ListRunsInput{WorkspaceID: "wsp_1", Limit: 4})
	require.NoError(t, err)
	require.Equal(t, []workflowrunstore.Run{startedHigh, startedLow, unstartedHigh, unstartedLow}, page.Items)
}

func TestGraphAndTimelineListsFenceParentAndPaginate(t *testing.T) {
	now := time.Date(2026, 7, 21, 10, 0, 0, 0, time.UTC)
	run := runFixture("wri_1", workflowrunstore.RunStatusRunning, now)
	loadCount := 0
	repository := repositoryStub{
		loadRun: func(_ context.Context, workspaceID, runID string) (workflowrunstore.Run, error) {
			loadCount++
			require.Equal(t, "wsp_1", workspaceID)
			require.Equal(t, run.ID, runID)
			return run, nil
		},
		listActivations: func(_ context.Context, params workflowrunstore.ListChildrenParams) ([]workflowrunstore.Activation, error) {
			require.Equal(t, workflowrunstore.ListChildrenParams{
				WorkspaceID: "wsp_1", RunID: run.ID, Status: workflowrunstore.ActivationStatusWaiting, Limit: 3,
			}, params)
			return []workflowrunstore.Activation{
				activationFixture("wra_1", run.ID, workflowrunstore.ActivationStatusWaiting, now),
				activationFixture("wra_2", run.ID, workflowrunstore.ActivationStatusWaiting, now),
			}, nil
		},
		listEffects: func(_ context.Context, params workflowrunstore.ListChildrenParams) ([]workflowrunstore.Effect, error) {
			require.Equal(t, workflowrunstore.ListChildrenParams{
				WorkspaceID: "wsp_1", RunID: run.ID, Status: workflowrunstore.EffectStatusAwaiting, Limit: 2,
			}, params)
			return []workflowrunstore.Effect{effectFixture("wfx_1", run.ID, workflowrunstore.EffectStatusAwaiting, now)}, nil
		},
		listEvents: func(_ context.Context, params workflowrunstore.ListEventsParams) ([]workflowrunstore.Event, error) {
			require.Equal(t, workflowrunstore.ListEventsParams{WorkspaceID: "wsp_1", RunID: run.ID, Limit: 2}, params)
			return []workflowrunstore.Event{eventFixture("wre_1", run.ID, 1, "run_accepted", now)}, nil
		},
	}
	service := New(repository)

	activations, err := service.ListActivations(context.Background(), ListChildrenInput{
		WorkspaceID: "wsp_1", RunID: run.ID, Status: workflowrunstore.ActivationStatusWaiting, Limit: 2,
	})
	require.NoError(t, err)
	require.Len(t, activations.Items, 2)

	effects, err := service.ListEffects(context.Background(), ListChildrenInput{
		WorkspaceID: "wsp_1", RunID: run.ID, Status: workflowrunstore.EffectStatusAwaiting, Limit: 1,
	})
	require.NoError(t, err)
	require.Len(t, effects.Items, 1)

	events, err := service.ListEvents(context.Background(), ListEventsInput{WorkspaceID: "wsp_1", RunID: run.ID, Limit: 1})
	require.NoError(t, err)
	require.Len(t, events.Items, 1)
	require.Equal(t, 3, loadCount)
}

func TestChildListReturnsNotFoundForMissingParentInsteadOfEmptySuccess(t *testing.T) {
	listCalled := false
	service := New(repositoryStub{
		loadRun: func(context.Context, string, string) (workflowrunstore.Run, error) {
			return workflowrunstore.Run{}, storeerr.NotFound("missing")
		},
		listEvents: func(context.Context, workflowrunstore.ListEventsParams) ([]workflowrunstore.Event, error) {
			listCalled = true
			return nil, nil
		},
	})
	_, err := service.ListEvents(context.Background(), ListEventsInput{WorkspaceID: "wsp_1", RunID: "wri_missing"})
	require.ErrorIs(t, err, storeerr.ErrNotFound)
	code, _, ok := ErrorDetails(err)
	require.True(t, ok)
	require.Equal(t, ErrorCodeNotFound, code)
	require.False(t, listCalled)
}

func TestCancelPersistsHumanProvenanceAndMakesReplayExplicit(t *testing.T) {
	now := time.Date(2026, 7, 21, 11, 12, 13, 456000000, time.FixedZone("offset", 8*60*60))
	canceled := runFixture("wri_1", workflowrunstore.RunStatusCanceled, now.UTC())
	event := eventFixture("wre_2", canceled.ID, 2, "run_canceled", now.UTC())
	var captured workflowrunstore.CancelRunParams
	service := New(repositoryStub{cancelRun: func(_ context.Context, params workflowrunstore.CancelRunParams) (workflowrunstore.Run, *workflowrunstore.Event, error) {
		captured = params
		return canceled, &event, nil
	}}, WithClock(fixedClock(now)))

	result, err := service.Cancel(context.Background(), CancelInput{
		WorkspaceID: " wsp_1 ", RunID: " wri_1 ", Reason: " user requested stop ",
		Actor: Actor{Type: " human ", ID: " usr_1 ", MemberID: " mem_1 "},
	})
	require.NoError(t, err)
	require.False(t, result.Replayed)
	require.Equal(t, &event, result.Event)
	require.Equal(t, "wsp_1", captured.WorkspaceID)
	require.Equal(t, "wri_1", captured.RunID)
	require.Equal(t, now.UTC(), captured.Now)
	require.JSONEq(t, `{
		"reason":"user requested stop",
		"actor":{"type":"human","id":"usr_1","memberId":"mem_1"}
	}`, string(captured.ReasonJSON))

	service = New(repositoryStub{cancelRun: func(context.Context, workflowrunstore.CancelRunParams) (workflowrunstore.Run, *workflowrunstore.Event, error) {
		return canceled, nil, nil
	}})
	result, err = service.Cancel(context.Background(), CancelInput{
		WorkspaceID: "wsp_1", RunID: "wri_1", Actor: Actor{Type: "human", ID: "usr_1", MemberID: "mem_1"},
	})
	require.NoError(t, err)
	require.True(t, result.Replayed)
	require.Nil(t, result.Event)
}

func TestCancelClassifiesCompletedTerminalAndConcurrentCancel(t *testing.T) {
	now := time.Now().UTC()
	actor := Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}
	for _, status := range []string{workflowrunstore.RunStatusSucceeded, workflowrunstore.RunStatusFailed} {
		t.Run(status, func(t *testing.T) {
			service := New(repositoryStub{
				cancelRun: func(context.Context, workflowrunstore.CancelRunParams) (workflowrunstore.Run, *workflowrunstore.Event, error) {
					return workflowrunstore.Run{}, nil, storeerr.Conflict("completed")
				},
				loadRun: func(context.Context, string, string) (workflowrunstore.Run, error) {
					return runFixture("wri_1", status, now), nil
				},
			})
			_, err := service.Cancel(context.Background(), CancelInput{WorkspaceID: "wsp_1", RunID: "wri_1", Actor: actor})
			require.ErrorIs(t, err, storeerr.ErrConflict)
			code, details, ok := ErrorDetails(err)
			require.True(t, ok)
			require.Equal(t, ErrorCodeTerminalConflict, code)
			require.Equal(t, TerminalConflictDetails{Status: status}, details)
		})
	}

	service := New(repositoryStub{
		cancelRun: func(context.Context, workflowrunstore.CancelRunParams) (workflowrunstore.Run, *workflowrunstore.Event, error) {
			return workflowrunstore.Run{}, nil, storeerr.Conflict("revision changed")
		},
		loadRun: func(context.Context, string, string) (workflowrunstore.Run, error) {
			return runFixture("wri_1", workflowrunstore.RunStatusCanceled, now), nil
		},
	})
	result, err := service.Cancel(context.Background(), CancelInput{WorkspaceID: "wsp_1", RunID: "wri_1", Actor: actor})
	require.NoError(t, err)
	require.True(t, result.Replayed)
}

func TestInvalidFiltersAndCursorAreRejectedBeforeRepository(t *testing.T) {
	service := New(repositoryStub{})
	_, err := service.ListRuns(context.Background(), ListRunsInput{WorkspaceID: "wsp_1", Status: "paused"})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)
	_, err = service.ListActivations(context.Background(), ListChildrenInput{WorkspaceID: "wsp_1", RunID: "wri_1", Status: workflowrunstore.EffectStatusPending})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)
	_, err = service.ListRuns(context.Background(), ListRunsInput{WorkspaceID: "wsp_1", Cursor: "not-base64"})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)
	_, err = service.Cancel(context.Background(), CancelInput{WorkspaceID: "wsp_1", RunID: "wri_1", Actor: Actor{Type: "service", ID: "svc_1", MemberID: "mem_1"}})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)
}

func TestCancelPersistsAuthenticatedAgentProvenance(t *testing.T) {
	now := time.Date(2026, 7, 22, 1, 30, 0, 0, time.UTC)
	canceled := runFixture("wri_1", workflowrunstore.RunStatusCanceled, now)
	event := eventFixture("wre_2", canceled.ID, 2, "run_canceled", now)
	var captured workflowrunstore.CancelRunParams
	service := New(repositoryStub{cancelRun: func(_ context.Context, params workflowrunstore.CancelRunParams) (workflowrunstore.Run, *workflowrunstore.Event, error) {
		captured = params
		return canceled, &event, nil
	}})

	_, err := service.Cancel(context.Background(), CancelInput{
		WorkspaceID: "wsp_1", RunID: "wri_1", Reason: "agent requested stop",
		Actor: Actor{Type: ActorTypeAgent, ID: "agt_1", MemberID: "mem_agent"},
	})
	require.NoError(t, err)
	require.JSONEq(t, `{
		"reason":"agent requested stop",
		"actor":{"type":"agent","id":"agt_1","memberId":"mem_agent"}
	}`, string(captured.ReasonJSON))
}

func TestRepositoryErrorsUseStableRunCodes(t *testing.T) {
	service := New(repositoryStub{loadRun: func(context.Context, string, string) (workflowrunstore.Run, error) {
		return workflowrunstore.Run{}, storeerr.NotFound("not found")
	}})
	_, err := service.GetRun(context.Background(), "wsp_1", "wri_1")
	require.ErrorIs(t, err, storeerr.ErrNotFound)
	code, _, ok := ErrorDetails(err)
	require.True(t, ok)
	require.Equal(t, ErrorCodeNotFound, code)
	require.False(t, errors.Is(err, storeerr.ErrConflict))
}

func runFixture(id, status string, now time.Time) workflowrunstore.Run {
	return workflowrunstore.Run{
		ID: id, WorkspaceID: "wsp_1", InvocationID: "wiv_1",
		PlanID: "pln_1", PlanVersionID: "plv_1", Status: status,
		InputJSON: json.RawMessage(`{"word":"画蛇添足"}`), StateJSON: json.RawMessage(`{"status":"running"}`),
		QueuedAt: now, Revision: 2, CreatedAt: now, UpdatedAt: now,
	}
}

func timePtr(value time.Time) *time.Time { return &value }

func activationFixture(id, runID, status string, now time.Time) workflowrunstore.Activation {
	return workflowrunstore.Activation{
		ID: id, WorkspaceID: "wsp_1", RunID: runID, LogicalKey: "a-" + id,
		NodeID: "node_1", Opcode: "send_message", Status: status,
		InputJSON: json.RawMessage(`{}`), ScopeContextJSON: json.RawMessage(`{}`), CreatedAt: now, UpdatedAt: now,
	}
}

func effectFixture(id, runID, status string, now time.Time) workflowrunstore.Effect {
	return workflowrunstore.Effect{
		ID: id, WorkspaceID: "wsp_1", RunID: runID, ActivationID: "wra_1",
		Kind: "send_message", Status: status, RequestJSON: json.RawMessage(`{}`), CreatedAt: now, UpdatedAt: now,
	}
}

func eventFixture(id, runID string, seq int64, eventType string, now time.Time) workflowrunstore.Event {
	return workflowrunstore.Event{
		ID: id, WorkspaceID: "wsp_1", RunID: runID, Seq: seq,
		Type: eventType, PayloadJSON: json.RawMessage(`{}`), CreatedAt: now,
	}
}
