import { describe, expect, it } from "vitest";
import {
  changeSetReviewReadModel,
  factoryRunDetailReadModel,
  realtimeProjectionEnvelope,
} from "@awp/application";
import {
  unsafeOpaqueId,
  type AgentRunId,
  type AttemptId,
  type ChangeSetId,
  type FactoryRunId,
  type PlanRevisionId,
  type PrincipalId,
  type ProjectId,
  type ProviderId,
  type ReviewId,
  type TaskId,
  type WorkspaceId,
} from "@awp/contracts";
import type { AgentRun, Attempt, ChangeSet, FactoryRun, Review, Task } from "@awp/domain";

const id = {
  project: unsafeOpaqueId<ProjectId>("project-i1-detail"),
  revision: unsafeOpaqueId<PlanRevisionId>("plan-revision-i1-detail"),
  task: unsafeOpaqueId<TaskId>("task-i1-detail"),
  factoryRun: unsafeOpaqueId<FactoryRunId>("factory-run-i1-detail"),
  agentRun: unsafeOpaqueId<AgentRunId>("agent-run-i1-detail"),
  attempt1: unsafeOpaqueId<AttemptId>("attempt-1"),
  attempt2: unsafeOpaqueId<AttemptId>("attempt-2"),
  workspace: unsafeOpaqueId<WorkspaceId>("workspace-i1-detail"),
  provider: unsafeOpaqueId<ProviderId>("provider-acp"),
  owner: unsafeOpaqueId<PrincipalId>("principal-owner"),
  reviewer: unsafeOpaqueId<PrincipalId>("principal-reviewer"),
  changeSet: unsafeOpaqueId<ChangeSetId>("changeset-i1-detail"),
  review: unsafeOpaqueId<ReviewId>("review-i1-detail"),
};

const task: Task = {
  id: id.task,
  projectId: id.project,
  planRevisionId: id.revision,
  title: "Implement authoritative detail projection",
  status: "waiting",
  dependencyIds: [],
  revision: 2,
};
const factoryRun: FactoryRun = {
  id: id.factoryRun,
  projectId: id.project,
  planRevisionId: id.revision,
  status: "retryable",
  reason: "execution resource disappeared",
  revision: 7,
};
const agentRun: AgentRun = {
  id: id.agentRun,
  factoryRunId: id.factoryRun,
  taskId: id.task,
  agentPrincipalId: unsafeOpaqueId<PrincipalId>("principal:agent:test"),
  status: "waiting",
  reason: "retry scheduled",
  revision: 3,
};
const attempts: readonly Attempt[] = [
  {
    id: id.attempt1,
    agentRunId: id.agentRun,
    workspaceId: id.workspace,
    status: "terminal",
    providerId: id.provider,
    accountId: "awp-dev",
    model: "model-a",
    selection: { kind: "initial", reason: "initial dispatch" },
    revision: 1,
  },
  {
    id: id.attempt2,
    agentRunId: id.agentRun,
    workspaceId: id.workspace,
    status: "created",
    providerId: id.provider,
    accountId: "awp-dev",
    model: "model-a",
    selection: { kind: "retry", reason: "recover preserved WIP", previousAttemptId: id.attempt1 },
    revision: 1,
  },
];

const detailInput = () => ({
  factoryRun,
  tasks: [task],
  agentRuns: [agentRun],
  attempts,
  currentAttemptIds: new Map([[id.agentRun, id.attempt2]]),
  workFacts: [
    {
      taskId: id.task,
      currentActorId: id.owner,
      stage: "recovery-ready",
      lastProgressAt: "2026-08-21T05:00:00Z",
      wait: {
        kind: "provider" as const,
        code: "executor-lost",
        summary: "Execution resource disappeared; retry is safe",
        since: "2026-08-21T05:01:00Z",
      },
      attention: {
        kind: "transient-recovery" as const,
        code: "retry-owned-by-awp",
        summary: "AWP owns retry",
        humanActionRequired: false,
      },
      nextAction: {
        kind: "automatic" as const,
        code: "retry-attempt",
        summary: "Start the immutable retry Attempt",
      },
    },
  ],
  stage: "recovering",
  lastProgressAt: "2026-08-21T05:00:00Z",
  wait: {
    kind: "provider" as const,
    code: "executor-lost",
    summary: "Execution resource disappeared",
    since: "2026-08-21T05:01:00Z",
  },
  failure: {
    failureClass: "execution-resource-loss",
    cause: "pod disappeared after heartbeat expiry",
    retryDisposition: "automatic" as const,
    exactSourceIdentity: "attempt-1@workspace-i1-detail",
    occurredAt: "2026-08-21T05:01:00Z",
    partial: false,
  },
  retryDisposition: "automatic" as const,
  wip: {
    state: "safe" as const,
    workspaceId: id.workspace,
    checkpointIdentity: "checkpoint-a7f3c9e",
    checkpointedAt: "2026-08-21T04:59:00Z",
    preservedFileCount: 14,
    cleanupAllowed: false,
    reason: "Recoverable WIP remains",
  },
  liveExecutorPresent: false,
  historicalAvailable: true,
  continuation: {
    factoryRunId: id.factoryRun,
    reattachable: true,
    continuationKey: "factory-run-i1-detail:attempt-2",
  },
});

describe("I1 authoritative detail read models", () => {
  it("preserves logical run, Attempt history, failure and WIP truth without a live executor", () => {
    const view = factoryRunDetailReadModel(detailInput());
    expect(view).toMatchObject({
      id: id.factoryRun,
      revision: 7,
      status: "retryable",
      liveExecutorPresent: false,
      historicalAvailable: true,
      retryDisposition: "automatic",
      wip: { state: "safe", cleanupAllowed: false },
      failure: { failureClass: "execution-resource-loss", partial: false },
    });
    expect(view.agentRuns[0]?.currentAttemptId).toBe(id.attempt2);
    expect(view.agentRuns[0]?.attempts.map((attempt) => attempt.id)).toEqual([
      id.attempt1,
      id.attempt2,
    ]);
    expect(view.work[0]?.attention).toMatchObject({
      kind: "transient-recovery",
      humanActionRequired: false,
    });
  });

  it("rejects a current Attempt that is not durable history of the AgentRun", () => {
    const wrong = unsafeOpaqueId<AttemptId>("attempt-other");
    expect(() =>
      factoryRunDetailReadModel({
        ...detailInput(),
        currentAttemptIds: new Map([[id.agentRun, wrong]]),
      }),
    ).toThrow(/does not belong/);
  });

  it("keeps protected Merge unavailable until exact candidate, Review, evidence, authority and target head are current", () => {
    const changeSet: ChangeSet = {
      id: id.changeSet,
      projectId: id.project,
      taskId: id.task,
      producerAttemptId: id.attempt2,
      baseIdentity: "base-abc",
      candidateDigest: "tree-123",
      candidateManifest: {
        treeDigest: "tree-123",
        patchDigest: "patch-123",
        changedPaths: ["src/queue.ts"],
        changes: [{ path: "src/queue.ts", kind: "modify" }],
      },
      revision: 4,
      status: "ready-to-merge",
    };
    const review: Review = {
      id: id.review,
      changeSetId: id.changeSet,
      candidateDigest: "tree-123",
      reviewerPrincipalId: id.reviewer,
      status: "submitted",
      disposition: "approved",
    };
    const base = {
      changeSet,
      reviews: [review],
      findings: [],
      evidence: [
        {
          id: "gate-domain",
          name: "Domain tests",
          state: "passed" as const,
          source: "debian1",
          observedAt: "2026-08-21T05:10:00Z",
          candidateDigest: "tree-123",
        },
      ],
      expectedTarget: { reference: "refs/heads/main", revision: "base-abc" },
      publicationFidelitySatisfied: true,
      policySatisfied: true,
      authoritySatisfied: true,
      targetHeadCurrent: true,
    };
    expect(changeSetReviewReadModel(base).mergeGate.action).toMatchObject({
      available: true,
      reasonCode: "ready",
    });
    expect(
      changeSetReviewReadModel({ ...base, targetHeadCurrent: false }).mergeGate.action,
    ).toMatchObject({
      available: false,
      reasonCode: "target-head",
    });
    expect(
      changeSetReviewReadModel({
        ...base,
        findings: [
          {
            id: "blocking",
            severity: "blocking",
            summary: "Cross-plan dependency bug",
            resolved: false,
          },
        ],
      }).mergeGate.action,
    ).toMatchObject({ available: false, reasonCode: "review" });
  });

  it("requires terminal result identity for a merged ChangeSet", () => {
    const changeSet: ChangeSet = {
      id: id.changeSet,
      projectId: id.project,
      taskId: id.task,
      producerAttemptId: id.attempt2,
      baseIdentity: "base-abc",
      candidateDigest: "tree-123",
      candidateManifest: {
        treeDigest: "tree-123",
        patchDigest: "patch-123",
        changedPaths: [],
        changes: [],
      },
      revision: 5,
      status: "merged",
    };
    expect(() =>
      changeSetReviewReadModel({
        changeSet,
        reviews: [],
        findings: [],
        evidence: [],
        expectedTarget: { reference: "refs/heads/main", revision: "result-456" },
        publicationFidelitySatisfied: true,
        policySatisfied: true,
        authoritySatisfied: true,
        targetHeadCurrent: true,
      }),
    ).toThrow(/requires resulting repository identity/);
  });

  it("creates monotonic-addressable realtime envelopes and rejects invalid sequence metadata", () => {
    const envelope = realtimeProjectionEnvelope({
      entityType: "factory-run",
      entityId: id.factoryRun,
      revision: 7,
      sequence: 42,
      observedAt: "2026-08-21T05:20:00Z",
      payload: { status: "retryable" },
    });
    expect(envelope).toMatchObject({ entityId: id.factoryRun, revision: 7, sequence: 42 });
    expect(() => realtimeProjectionEnvelope({ ...envelope, sequence: -1 })).toThrow(
      /non-negative integer/,
    );
  });
});
