import { describe, expect, it } from "vitest";
import type { AttemptRepository } from "@awp/application";
import type { Attempt } from "@awp/domain";
import {
  unsafeOpaqueId,
  type AgentRunId,
  type AttemptId,
  type ConnectionId,
  type CorrelationId,
  type CredentialReferenceId,
  type OperationId,
  type PrincipalId,
  type ProviderId,
  type ProviderOperationContext,
  type TaskId,
  type WorkspaceId,
} from "@awp/contracts";
import {
  ACP_PROVIDER_ID,
  AcpAgentProvider,
  type AcpNativeClient,
  type AcpNativeSession,
} from "../../../packages/providers/agent-acp/src/index.js";

function context(): ProviderOperationContext {
  return {
    operationId: unsafeOpaqueId<OperationId>("op-agent-1"),
    idempotencyKey: "idem-agent-1",
    correlationId: unsafeOpaqueId<CorrelationId>("corr-agent-1"),
    connectionId: unsafeOpaqueId<ConnectionId>("connection-acp"),
    credentialReferenceId: unsafeOpaqueId<CredentialReferenceId>(
      "credential-must-not-enter-session",
    ),
    authority: {
      principal: {
        id: unsafeOpaqueId<PrincipalId>("principal-system"),
        kind: "system",
        capabilities: [],
      },
      capabilities: [],
    },
  };
}

const attemptId = unsafeOpaqueId<AttemptId>("attempt-1");
const agentRunId = unsafeOpaqueId<AgentRunId>("agent-run-1");
const taskId = unsafeOpaqueId<TaskId>("task-1");
const workspaceId = unsafeOpaqueId<WorkspaceId>("workspace-1");

function canonicalAttempt(overrides: Partial<Attempt> = {}): Attempt {
  return {
    id: attemptId,
    agentRunId,
    workspaceId,
    status: "created",
    providerId: ACP_PROVIDER_ID,
    accountId: "subrouter:codex-1",
    model: "gpt-5.3-codex-spark",
    revision: 1,
    ...overrides,
  };
}

function attemptRepository(initial: Attempt | undefined = canonicalAttempt()): AttemptRepository {
  let attempt = initial;
  return {
    async getById(id) {
      return attempt?.id === id ? attempt : undefined;
    },
    async insert(value) {
      attempt = value;
    },
    async update(value) {
      attempt = value;
    },
    async listByAgentRunIds(ids) {
      return attempt && ids.includes(attempt.agentRunId) ? [attempt] : [];
    },
    async bindProviderReference(id, reference) {
      if (!attempt || attempt.id !== id) throw new Error("Attempt does not exist");
      attempt = { ...attempt, providerReference: reference, revision: attempt.revision + 1 };
      return attempt;
    },
  };
}

class FakeAcpClient implements AcpNativeClient {
  session?: AcpNativeSession;
  startCalls = 0;
  cancelCalls = 0;
  lastStartInput?: Readonly<Record<string, unknown>>;
  dropStartResponseOnce = false;
  nativeCapabilities: readonly string[] = ["session.new", "session.cancel", "tool-call"];

  async capabilities() {
    return {
      protocolVersion: "1.19",
      agentName: "codex-acp",
      capabilities: this.nativeCapabilities,
    };
  }

  async findSessionByIdempotencyKey(): Promise<AcpNativeSession | undefined> {
    return this.session;
  }

  async startSession(input: {
    readonly attemptId: string;
    readonly agentRunId: string;
    readonly taskId: string;
    readonly workspaceId: string;
    readonly providerId: string;
    readonly accountId: string;
    readonly model: string;
    readonly idempotencyKey: string;
    readonly correlationId: string;
    readonly selectionKind?: "initial" | "retry" | "fallback";
    readonly failureInjection?: "dogfood-post-prompt-failure-17";
  }): Promise<AcpNativeSession> {
    this.startCalls += 1;
    this.lastStartInput = input;
    this.session = {
      id: "acp-session-1",
      attemptId: input.attemptId,
      agentRunId: input.agentRunId,
      taskId: input.taskId,
      workspaceId: input.workspaceId,
      providerId: input.providerId,
      accountId: input.accountId,
      model: input.model,
      status: "running",
      protocolVersion: "1.19",
      agentName: "codex-acp",
      capabilities: ["session.cancel", "tool-call"],
    };
    if (this.dropStartResponseOnce) {
      this.dropStartResponseOnce = false;
      throw { status: 503 };
    }
    return this.session;
  }

  async cancelSession(): Promise<AcpNativeSession> {
    this.cancelCalls += 1;
    if (this.session === undefined) throw { status: 404 };
    this.session = { ...this.session, status: "cancelled" };
    return this.session;
  }

  async getSession(): Promise<AcpNativeSession | undefined> {
    return this.session;
  }
}

describe("AcpAgentProvider", () => {
  it("discovers ACP capabilities through the adapter descriptor", async () => {
    const descriptor = await new AcpAgentProvider(
      new FakeAcpClient(),
      attemptRepository(),
    ).describe();
    expect(descriptor.capabilities).toContain("acp.session.cancel");
    expect(descriptor.adapterVersion).toContain("acp-v1");
  });

  it("RT-033 does not advertise an AWP action omitted by authoritative ACP capabilities", async () => {
    const client = new FakeAcpClient();
    client.nativeCapabilities = ["session.new", "tool-call"];

    const descriptor = await new AcpAgentProvider(client, attemptRepository()).describe();

    expect(descriptor.capabilities).toContain("agent.start");
    expect(descriptor.capabilities).not.toContain("agent.cancel");
    expect(descriptor.capabilities).not.toContain("acp.session.cancel");
  });

  it("RT-016-B maps an Attempt without projecting reusable credential authority", async () => {
    const client = new FakeAcpClient();
    const result = await new AcpAgentProvider(client, attemptRepository()).startAttempt(
      context(),
      attemptId,
      agentRunId,
      taskId,
      workspaceId,
    );

    expect(result.value).toMatchObject({
      state: "running",
      details: {
        attemptId: "attempt-1",
        agentRunId: "agent-run-1",
        taskId: "task-1",
        workspaceId: "workspace-1",
        providerId: "provider:acp",
        accountId: "subrouter:codex-1",
        model: "gpt-5.3-codex-spark",
        agentName: "codex-acp",
      },
    });
    expect(client.lastStartInput).not.toHaveProperty("credentialReferenceId");
    expect(client.lastStartInput).not.toHaveProperty("credential");
    expect(client.lastStartInput).not.toHaveProperty("token");
  });

  it("propagates injection only for an explicit initial selection", async () => {
    const initialClient = new FakeAcpClient();
    await new AcpAgentProvider(
      initialClient,
      attemptRepository(
        canonicalAttempt({ selection: { kind: "initial", reason: "dogfood initial dispatch" } }),
      ),
    ).startAttempt(context(), attemptId, agentRunId, taskId, workspaceId);
    expect(initialClient.lastStartInput).toMatchObject({ selectionKind: "initial" });
    expect(initialClient.lastStartInput).not.toHaveProperty("failureInjection");

    for (const attempt of [
      canonicalAttempt({ selection: { kind: "retry", reason: "retry" } }),
      canonicalAttempt({ selection: undefined } as Partial<Attempt>),
    ]) {
      const client = new FakeAcpClient();
      await new AcpAgentProvider(client, attemptRepository(attempt)).startAttempt(
        context(),
        attemptId,
        agentRunId,
        taskId,
        workspaceId,
      );
      expect(client.lastStartInput).not.toHaveProperty("failureInjection");
    }
  });

  it("replays session start idempotently", async () => {
    const client = new FakeAcpClient();
    const provider = new AcpAgentProvider(client, attemptRepository());
    await provider.startAttempt(context(), attemptId, agentRunId, taskId, workspaceId);
    await provider.startAttempt(context(), attemptId, agentRunId, taskId, workspaceId);
    expect(client.startCalls).toBe(1);
  });

  it("B-FIRE-MUT-002 reconciles ambiguous ACP session start before repeating mutation", async () => {
    const client = new FakeAcpClient();
    client.dropStartResponseOnce = true;
    const provider = new AcpAgentProvider(client, attemptRepository());

    await expect(
      provider.startAttempt(context(), attemptId, agentRunId, taskId, workspaceId),
    ).rejects.toMatchObject({
      providerError: { category: "unavailable", retryable: true },
    });
    expect(client.startCalls).toBe(1);
    expect(client.session?.status).toBe("running");

    const retry = await provider.startAttempt(
      context(),
      attemptId,
      agentRunId,
      taskId,
      workspaceId,
    );
    expect(retry.value.state).toBe("running");
    expect(client.startCalls).toBe(1);
  });

  it("rejects idempotency collision with a different immutable Attempt mapping", async () => {
    const client = new FakeAcpClient();
    client.session = {
      id: "acp-session-foreign",
      attemptId: "attempt-other",
      agentRunId: "agent-run-1",
      taskId: "task-1",
      workspaceId: "workspace-1",
      providerId: "provider:acp",
      accountId: "subrouter:codex-1",
      model: "gpt-5.3-codex-spark",
      status: "running",
      protocolVersion: "1.19",
      agentName: "codex-acp",
      capabilities: [],
    };

    await expect(
      new AcpAgentProvider(client, attemptRepository()).startAttempt(
        context(),
        attemptId,
        agentRunId,
        taskId,
        workspaceId,
      ),
    ).rejects.toMatchObject({ providerError: { category: "conflict-stale" } });
  });

  it("fails closed when canonical C1 Attempt provenance is incomplete", async () => {
    const client = new FakeAcpClient();
    const incomplete = canonicalAttempt({ accountId: undefined });
    await expect(
      new AcpAgentProvider(client, attemptRepository(incomplete)).startAttempt(
        context(),
        attemptId,
        agentRunId,
        taskId,
        workspaceId,
      ),
    ).rejects.toMatchObject({ providerError: { category: "adapter-protocol" } });
    expect(client.startCalls).toBe(0);
  });

  it("fails closed when canonical C1 Attempt is assigned to a different provider", async () => {
    const client = new FakeAcpClient();
    const other = canonicalAttempt({
      providerId: unsafeOpaqueId<ProviderId>("provider:other"),
    });
    await expect(
      new AcpAgentProvider(client, attemptRepository(other)).startAttempt(
        context(),
        attemptId,
        agentRunId,
        taskId,
        workspaceId,
      ),
    ).rejects.toMatchObject({ providerError: { category: "unsupported-capability" } });
    expect(client.startCalls).toBe(0);
  });

  it("RT-034-B keeps provider terminal state semantic-neutral for Task completion", async () => {
    const client = new FakeAcpClient();
    client.session = {
      id: "acp-session-completed",
      attemptId: "attempt-1",
      agentRunId: "agent-run-1",
      taskId: "task-1",
      workspaceId: "workspace-1",
      providerId: "provider:acp",
      accountId: "subrouter:codex-1",
      model: "gpt-5.3-codex-spark",
      status: "completed",
      protocolVersion: "1.19",
      agentName: "codex-acp",
      capabilities: [],
    };

    const reconciled = await new AcpAgentProvider(client, attemptRepository()).reconcile({
      context: context(),
      reference: {
        providerId: ACP_PROVIDER_ID,
        resourceType: "agent-session",
        nativeId: "acp-session-completed",
        observedAt: new Date().toISOString(),
      },
    });

    expect(reconciled.value.state).toBe("completed");
    expect(reconciled.value.details).not.toHaveProperty("taskStatus");
    expect(reconciled.value.details).not.toHaveProperty("taskCompleted");
    expect(reconciled.value.details).not.toHaveProperty("semanticOutcome");
  });

  it("fails closed when cancellation is requested before an ACP provider reference exists", async () => {
    await expect(
      new AcpAgentProvider(new FakeAcpClient(), attemptRepository()).cancelAttempt(
        context(),
        attemptId,
      ),
    ).rejects.toMatchObject({
      providerError: { category: "missing-resource", retryable: false },
    });
  });

  it("cancels a resolved native ACP session replay-safely", async () => {
    const client = new FakeAcpClient();
    const provider = new AcpAgentProvider(client, attemptRepository());
    await provider.startAttempt(context(), attemptId, agentRunId, taskId, workspaceId);
    const cancelled = await provider.cancelNativeSession("acp-session-1");
    const replay = await provider.cancelNativeSession("acp-session-1");

    expect(cancelled.value.state).toBe("cancelled");
    expect(replay.value.state).toBe("cancelled");
    expect(client.cancelCalls).toBe(1);
  });

  it("binds the native session to the canonical Attempt and cancels by Attempt identity", async () => {
    const client = new FakeAcpClient();
    const attempts = attemptRepository();
    const provider = new AcpAgentProvider(client, attempts);
    const started = await provider.startAttempt(
      context(),
      attemptId,
      agentRunId,
      taskId,
      workspaceId,
    );
    const bound = await attempts.getById(attemptId);
    expect(bound?.providerReference?.nativeId).toBe(started.references[0]?.nativeId);

    const cancelled = await provider.cancelAttempt(context(), attemptId);
    expect(cancelled.value.state).toBe("cancelled");
    expect(client.cancelCalls).toBe(1);
  });
});
