import { describe, expect, it } from "vitest";
import { unsafeOpaqueId, type CredentialReferenceId, type OperationId } from "@awp/contracts";
import {
  GitHubCIProvider,
  GitHubRepositoryRegistry,
  operationKeyFor,
  type GitHubActionsTransport,
  type GitHubRepositoryConfig,
  type GitHubWorkflowRun,
} from "../../../packages/providers/ci-github/src/index.js";
import { providerContext } from "../../security/execution/provider-context.js";

const repository: GitHubRepositoryConfig = {
  repositoryKey: "awp",
  owner: "platform-modules",
  repository: "awp",
  workflowId: "ci.yml",
  dispatchRef: "main",
};

class FakeGitHub implements GitHubActionsTransport {
  readonly runs: GitHubWorkflowRun[] = [];
  dispatches = 0;
  throwAfterDispatch = false;
  required = ["typecheck", "test"];

  async dispatch(
    _config: GitHubRepositoryConfig,
    _credentialReferenceId: CredentialReferenceId,
    operationKey: string,
    candidateDigest: string,
  ): Promise<void> {
    this.dispatches += 1;
    this.runs.push({
      id: 1000 + this.dispatches,
      name: "CI",
      displayTitle: operationKey,
      status: "queued",
      headSha: candidateDigest,
      runAttempt: 1,
      updatedAt: "2026-08-20T00:00:00.000Z",
    });
    if (this.throwAfterDispatch) {
      this.throwAfterDispatch = false;
      throw new Error("timeout after workflow_dispatch accepted");
    }
  }

  async listRuns(): Promise<readonly GitHubWorkflowRun[]> {
    return this.runs;
  }

  async getRun(
    _config: GitHubRepositoryConfig,
    _credentialReferenceId: CredentialReferenceId,
    runId: number,
  ): Promise<GitHubWorkflowRun | undefined> {
    return this.runs.find((run) => run.id === runId);
  }

  async cancelRun(
    _config: GitHubRepositoryConfig,
    _credentialReferenceId: CredentialReferenceId,
    runId: number,
  ): Promise<void> {
    const index = this.runs.findIndex((run) => run.id === runId);
    if (index < 0) return;
    const run = this.runs[index]!;
    this.runs[index] = { ...run, status: "completed", conclusion: "cancelled" };
  }

  async requiredChecks(): Promise<readonly string[]> {
    return this.required;
  }
}

describe("GitHub CIProvider", () => {
  it("correlates dispatches to deterministic operation/candidate identity and suppresses duplicate dispatch", async () => {
    const github = new FakeGitHub();
    const provider = new GitHubCIProvider(
      github,
      new GitHubRepositoryRegistry([repository]),
      async () => {},
    );
    const context = {
      ...providerContext(),
      operationId: unsafeOpaqueId<OperationId>("ci-operation-1"),
    };
    const candidate = "abcdef1234567890";

    const first = await provider.start(context, repository.repositoryKey, candidate);
    const second = await provider.start(context, repository.repositoryKey, candidate);

    expect(github.dispatches).toBe(1);
    expect(first.value.details.candidateDigest).toBe(candidate);
    expect(second.references[0]?.nativeId).toBe(first.references[0]?.nativeId);
    expect(operationKeyFor(context.operationId, candidate)).toBe("awp:ci-operation-1:abcdef123456");
  });

  it("reconciles a timeout after GitHub accepted workflow_dispatch", async () => {
    const github = new FakeGitHub();
    github.throwAfterDispatch = true;
    const provider = new GitHubCIProvider(
      github,
      new GitHubRepositoryRegistry([repository]),
      async () => {},
    );

    const result = await provider.start(
      providerContext(),
      repository.repositoryKey,
      "1234567abcdef",
    );
    expect(result.value.state).toBe("queued");
    expect(github.dispatches).toBe(1);
  });

  it("cancels and reconciles the concrete provider run", async () => {
    const github = new FakeGitHub();
    const provider = new GitHubCIProvider(
      github,
      new GitHubRepositoryRegistry([repository]),
      async () => {},
    );
    const started = await provider.start(
      providerContext(),
      repository.repositoryKey,
      "1234567abcdef",
    );
    const reference = started.references[0]!;

    const cancelled = await provider.cancel(providerContext(), reference);
    expect(cancelled.value.state).toBe("cancelled");

    const reconciled = await provider.reconcile({ context: providerContext(), reference });
    expect(reconciled.value.state).toBe("cancelled");
  });

  it("consumes actual GitHub required checks instead of manufacturing an internal green receipt", async () => {
    const github = new FakeGitHub();
    const provider = new GitHubCIProvider(github, new GitHubRepositoryRegistry([repository]));
    expect(await provider.listRequiredChecks(providerContext(), "awp", "main")).toEqual([
      "typecheck",
      "test",
    ]);
  });

  it("rejects non-revision candidate identities", async () => {
    const github = new FakeGitHub();
    const provider = new GitHubCIProvider(github, new GitHubRepositoryRegistry([repository]));
    await expect(provider.start(providerContext(), "awp", "main")).rejects.toThrow(
      "candidate digest",
    );
    expect(github.dispatches).toBe(0);
  });
});
