import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import {
  WorkspaceExecutionDispatcher,
  type AgentProvider,
  type ApplicationTransaction,
  type UnitOfWork,
  type WorkspaceProvider,
} from "@awp/application";
import {
  authorityContext,
  unsafeOpaqueId,
  type AgentRunId,
  type AttemptId,
  type ConnectionId,
  type CorrelationId,
  type CredentialReferenceId,
  type FactoryRunId,
  type OperationId,
  type PrincipalId,
  type ProjectId,
  type ProviderId,
  type TaskId,
  type WorkspaceId,
} from "@awp/contracts";
import type { AgentRun, Attempt, FactoryRun, Task, Workspace } from "@awp/domain";

describe("GOLIVE immutable Attempt retry", () => {
  it("preserves the failed Attempt and starts a retry on the same AgentRun and Workspace", async () => {
    const ids = {
      project: unsafeOpaqueId<ProjectId>("project-retry"),
      task: unsafeOpaqueId<TaskId>("task-retry"),
      factory: unsafeOpaqueId<FactoryRunId>("factory-retry"),
      agent: unsafeOpaqueId<AgentRunId>("agent-retry"),
      workspace: unsafeOpaqueId<WorkspaceId>("workspace-retry"),
      failed: unsafeOpaqueId<AttemptId>("attempt-failed"),
      retry: unsafeOpaqueId<AttemptId>("attempt-retry"),
      provider: unsafeOpaqueId<ProviderId>("provider:acp"),
    };
    const task: Task = {
      id: ids.task,
      projectId: ids.project,
      planRevisionId: unsafeOpaqueId("revision-retry"),
      title: "Retry preserved work",
      status: "executing",
      position: 0,
      dependencyIds: [],
      revision: 1,
    };
    const factoryRun: FactoryRun = {
      id: ids.factory,
      projectId: ids.project,
      planRevisionId: task.planRevisionId,
      taskId: ids.task,
      status: "running",
      revision: 1,
    };
    const agentRun: AgentRun = {
      id: ids.agent,
      factoryRunId: ids.factory,
      taskId: ids.task,
      agentPrincipalId: unsafeOpaqueId<PrincipalId>("principal:agent:retry"),
      role: "coder",
      status: "active",
      revision: 1,
    };
    const failed: Attempt = {
      id: ids.failed,
      agentRunId: ids.agent,
      workspaceId: ids.workspace,
      status: "running",
      providerId: ids.provider,
      accountId: "zync2",
      model: "gpt-5.3-codex-spark",
      selection: { kind: "initial", reason: "initial dispatch" },
      revision: 1,
    };
    const attempts = new Map<AttemptId, Attempt>([[failed.id, failed]]);
    let currentWorkspace: Workspace = { id: ids.workspace, projectId: ids.project, revision: 1 };
    let currentAgent = agentRun;
    let currentFactory = factoryRun;
    let launchEnvironment: Readonly<Record<string, string>> | undefined;
    let replacementCount = 0;
    let attestationCount = 0;
    let agentStartCount = 0;
    const providerOrder: string[] = [];
    const events: Array<Record<string, unknown>> = [];
    const tx = {
      attempts: {
        getById: async (id: AttemptId) => attempts.get(id),
        listByAgentRunIds: async () => [...attempts.values()],
        insert: async (value: Attempt) => void attempts.set(value.id, value),
        update: async (value: Attempt) => void attempts.set(value.id, value),
      },
      workspaces: {
        getById: async () => currentWorkspace,
        update: async (value: Workspace) => void (currentWorkspace = value),
      },
      agentRuns: {
        getById: async () => currentAgent,
        update: async (value: AgentRun) => void (currentAgent = value),
      },
      factoryRuns: {
        getById: async () => currentFactory,
        update: async (value: FactoryRun) => void (currentFactory = value),
      },
      tasks: { getById: async () => task },
      events: { append: async (value: Record<string, unknown>) => void events.push(value) },
      audit: { append: async () => undefined },
      outbox: { append: async () => undefined },
    } as unknown as ApplicationTransaction;
    const uow: UnitOfWork = { transaction: async (work) => work(tx) };
    const workspaceProvider = {
      attestCheckpoint: async (_context, workspaceId, _profile, digest, observedAt) => {
        attestationCount += 1;
        providerOrder.push("attest");
        expect(workspaceId).toBe(ids.workspace);
        expect(digest).toBe("c".repeat(40));
        expect(observedAt).toBe("2026-08-21T00:00:01.000Z");
        return { value: { state: "checkpoint-attested", details: {} }, references: [], observedAt };
      },
      replaceCompute: async (_context, workspaceId, _profile, agentRunId, launch) => {
        replacementCount += 1;
        providerOrder.push("replace");
        expect(workspaceId).toBe(ids.workspace);
        expect(agentRunId).toBe(ids.agent);
        launchEnvironment = launch?.environment;
        return {
          value: {
            state: "preparing",
            details: {
              workspaceId,
              image: "ghcr.io/platform-modules/awp-agent-runner@sha256:" + "a".repeat(64),
              immutableImageDigest: true,
            },
          },
          references: [
            {
              providerId: ids.provider,
              resourceType: "Workspace",
              nativeId: "awp-workspace-retry",
              nativeRevision: "pod-rv-2",
              observedAt: "2026-08-21T00:00:00.000Z",
            },
          ],
          observedAt: "2026-08-21T00:00:00.000Z",
          reconciliationToken: "awp-workspace-retry:pod-rv-2",
        };
      },
    } as WorkspaceProvider;
    const agentProvider = {
      startAttempt: async (_context, attemptId, agentRunId, taskId, workspaceId) => {
        agentStartCount += 1;
        providerOrder.push("agent-start");
        expect(attemptId).toBe(ids.retry);
        expect(agentRunId).toBe(ids.agent);
        expect(taskId).toBe(ids.task);
        expect(workspaceId).toBe(ids.workspace);
        return {
          value: { state: "running", details: { attemptId } },
          references: [],
          observedAt: "2026-08-21T00:00:02.000Z",
        };
      },
    } as AgentProvider;
    const dispatcher = new WorkspaceExecutionDispatcher(
      uow,
      workspaceProvider,
      { next: () => ids.retry },
      { now: () => new Date("2026-08-21T00:00:00.000Z") },
      {
        profileKey: "golive-native",
        callbackBaseUrl: "http://control-plane",
        callbackSecret: "retry-secret",
        agentProviderId: ids.provider,
        accountId: "zync2",
        model: "gpt-5.3-codex-spark",
        connectionId: unsafeOpaqueId("connection:kubernetes"),
        credentialReferenceId: unsafeOpaqueId<CredentialReferenceId>("credential:kubernetes"),
        agentProvider,
        agentConnectionId: unsafeOpaqueId<ConnectionId>("connection:agent-acp"),
        agentCredentialReferenceId: unsafeOpaqueId<CredentialReferenceId>("credential:agent-acp"),
      },
    );
    const context = {
      operationId: unsafeOpaqueId<OperationId>("operation-retry"),
      correlationId: unsafeOpaqueId<CorrelationId>("correlation-retry"),
      idempotencyKey: "retry",
      authority: authorityContext(
        {
          id: unsafeOpaqueId<PrincipalId>("principal:owner"),
          kind: "human",
          capabilities: [],
        },
        [],
        ids.project,
      ),
    };
    const token = createHash("sha256").update(`retry-secret:${ids.failed}`).digest("hex");
    const retry = await dispatcher.failAttempt(
      {
        attemptId: ids.failed,
        token,
        reason: "injected non-zero agent exit 17",
        workspaceCheckpointDigest: "c".repeat(40),
        workspaceCheckpointedAt: "2026-08-21T00:00:01.000Z",
      },
      context,
    );

    expect(attempts).toHaveLength(2);
    expect(attempts.get(ids.failed)).toMatchObject({
      status: "terminal",
      reason: "injected non-zero agent exit 17",
    });
    expect(retry).toMatchObject({
      id: ids.retry,
      agentRunId: ids.agent,
      workspaceId: ids.workspace,
      status: "running",
      selection: { kind: "retry", previousAttemptId: ids.failed },
    });
    expect(launchEnvironment).toMatchObject({
      AWP_ATTEMPT_ID: ids.retry,
      AWP_AGENT_RUN_ID: ids.agent,
      AWP_TASK_ID: ids.task,
    });
    expect(currentAgent.status).toBe("active");
    expect(currentFactory.status).toBe("running");
    expect(providerOrder.slice(0, 3)).toEqual(["attest", "replace", "agent-start"]);
    expect(agentStartCount).toBe(1);
    expect(events).toContainEqual(
      expect.objectContaining({
        type: "RetryComputeReplaced",
        aggregateType: "Attempt",
        aggregateId: ids.retry,
        payload: expect.objectContaining({
          previousAttemptId: ids.failed,
          agentRunId: ids.agent,
          taskId: ids.task,
          workspaceId: ids.workspace,
          workspaceProviderState: "preparing",
          workspaceProviderDetails: expect.objectContaining({
            workspaceId: ids.workspace,
            immutableImageDigest: true,
          }),
          workspaceProviderReferences: [
            expect.objectContaining({
              nativeId: "awp-workspace-retry",
              nativeRevision: "pod-rv-2",
            }),
          ],
          replacementReconciliationToken: "awp-workspace-retry:pod-rv-2",
        }),
      }),
    );
    expect(currentWorkspace).toMatchObject({
      checkpointDigest: "c".repeat(40),
      checkpointSource: "git-tree",
    });

    const repeated = await dispatcher.failAttempt(
      {
        attemptId: ids.failed,
        token,
        reason: "duplicate failure callback",
        workspaceCheckpointDigest: "c".repeat(40),
        workspaceCheckpointedAt: "2026-08-21T00:00:01.000Z",
      },
      context,
    );
    expect(repeated).toEqual(retry);
    expect(replacementCount).toBe(1);
    expect(attestationCount).toBe(2);
    expect(agentStartCount).toBe(1);
    expect(attempts).toHaveLength(2);
  });
});
