import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import {
  capability,
  unsafeOpaqueId,
  type ChangeSetId,
  type ConnectionId,
  type CorrelationId,
  type CredentialReferenceId,
  type FactoryRunId,
  type OperationId,
  type PrincipalId,
  type ProviderOperationContext,
} from "@awp/contracts";
import {
  GitHubAdapterError,
  GitHubForgeProvider,
  GitHubTrustedMerger,
  GitHubTrustedPublisher,
  type GitHubMergeState,
  type GitHubPublicationState,
  type GitHubRepositoryState,
  type GitHubTrustedClient,
} from "../../../packages/providers/vcs-github/src/index.js";

function context(kind: "system" | "human" | "agent" = "system"): ProviderOperationContext {
  return {
    operationId: unsafeOpaqueId<OperationId>("op-publish-1"),
    idempotencyKey: "idem-publish-1",
    correlationId: unsafeOpaqueId<CorrelationId>("corr-publish-1"),
    connectionId: unsafeOpaqueId<ConnectionId>("connection-github-app"),
    credentialReferenceId: unsafeOpaqueId<CredentialReferenceId>("credential-github-app"),
    authority: {
      principal: {
        id: unsafeOpaqueId<PrincipalId>(`principal-${kind}`),
        kind,
        capabilities: [capability("change.publish")],
      },
      capabilities: [capability("change.publish")],
    },
  };
}

const diff =
  "diff --git a/packages/example.ts b/packages/example.ts\n--- a/packages/example.ts\n+++ b/packages/example.ts\n@@ -1 +1 @@\n-old\n+new\n";
const candidateManifest = {
  treeDigest: "candidate-tree-a",
  patchDigest: createHash("sha256").update(diff).digest("hex"),
  changedPaths: ["packages/example.ts"],
  changes: [{ path: "packages/example.ts", kind: "modify" as const }],
} as const;

class FakeGitHubClient implements GitHubTrustedClient {
  repository: GitHubRepositoryState = {
    repositoryKey: "platform-modules/awp",
    nativeId: "repo-1",
    defaultBranch: "main",
    headRevision: "base-a",
  };
  publication?: GitHubPublicationState;
  merge?: GitHubMergeState;
  publishCalls = 0;
  mergeCalls = 0;
  dropMergeResponseOnce = false;
  dropPublishResponseOnce = false;
  lastPublishInput?: Readonly<Record<string, unknown>>;

  async inspectRepository(): Promise<GitHubRepositoryState> {
    return this.repository;
  }

  async findPublicationByIdempotencyKey(): Promise<GitHubPublicationState | undefined> {
    return this.publication;
  }

  async publishCandidate(input: {
    readonly repositoryKey: string;
    readonly targetRef: string;
    readonly expectedBaseRevision: string;
    readonly candidateDigest: string;
    readonly candidateManifest: typeof candidateManifest;
    readonly diff: string;
    readonly changeSetId: string;
    readonly factoryRunId: string;
    readonly idempotencyKey: string;
    readonly credentialReferenceId: CredentialReferenceId;
  }): Promise<GitHubPublicationState> {
    this.publishCalls += 1;
    this.lastPublishInput = input;
    this.publication = {
      nativeId: "publication-1",
      repositoryKey: input.repositoryKey,
      targetRef: input.targetRef,
      targetRevision: "publication-commit-a",
      baseRevision: input.expectedBaseRevision,
      candidateDigest: input.candidateDigest,
      candidateManifest: input.candidateManifest,
      factoryRunId: input.factoryRunId,
      idempotencyKey: input.idempotencyKey,
      status: "published",
    };
    if (this.dropPublishResponseOnce) {
      this.dropPublishResponseOnce = false;
      throw { status: 503 };
    }
    return this.publication;
  }

  async readPublication(): Promise<GitHubPublicationState | undefined> {
    return this.publication;
  }

  async findMergeByIdempotencyKey(): Promise<GitHubMergeState | undefined> {
    return this.merge;
  }

  async mergePublication(input: {
    readonly repositoryKey: string;
    readonly targetRef: string;
    readonly expectedTargetRevision: string;
    readonly expectedBaseRevision: string;
    readonly publicationNativeId: string;
    readonly candidateDigest: string;
    readonly candidateManifest: typeof candidateManifest;
    readonly idempotencyKey: string;
    readonly credentialReferenceId: CredentialReferenceId;
  }): Promise<GitHubMergeState> {
    this.mergeCalls += 1;
    this.merge = {
      nativeId: "merge-1",
      repositoryKey: input.repositoryKey,
      targetRef: input.targetRef,
      expectedTargetRevision: input.expectedTargetRevision,
      expectedBaseRevision: input.expectedBaseRevision,
      publicationNativeId: input.publicationNativeId,
      candidateDigest: input.candidateDigest,
      candidateManifest: input.candidateManifest,
      resultingRevision: "merge-revision-1",
      status: "merged",
    };
    if (this.dropMergeResponseOnce) {
      this.dropMergeResponseOnce = false;
      throw { status: 503 };
    }
    return this.merge;
  }

  async readMerge(): Promise<GitHubMergeState | undefined> {
    return this.merge;
  }
}

function request(ctx = context()) {
  return {
    context: ctx,
    changeSetId: unsafeOpaqueId<ChangeSetId>("change-set-1"),
    factoryRunId: unsafeOpaqueId<FactoryRunId>("factory-run-1"),
    repositoryKey: "platform-modules/awp",
    baseRevision: "base-a",
    candidateDigest: "candidate-tree-a",
    candidateManifest,
    diff,
  };
}

describe("GitHub providers", () => {
  it("maps repository inspection with immutable head evidence", async () => {
    const result = await new GitHubForgeProvider(new FakeGitHubClient()).inspectRepository(
      context(),
      "platform-modules/awp",
    );
    expect(result.value).toEqual({ defaultBranch: "main", headRevision: "base-a" });
    expect(result.references[0]).toMatchObject({
      resourceType: "repository",
      nativeId: "repo-1",
      nativeRevision: "base-a",
    });
  });

  it("RT-005 consumes exact publication identity without repository-shaped workspace state", async () => {
    const client = new FakeGitHubClient();
    await new GitHubTrustedPublisher(client).publish(request());

    expect(client.lastPublishInput).toEqual({
      repositoryKey: "platform-modules/awp",
      targetRef: "refs/heads/awp/change-set-1",
      expectedBaseRevision: "base-a",
      candidateDigest: "candidate-tree-a",
      candidateManifest,
      diff,
      changeSetId: "change-set-1",
      factoryRunId: "factory-run-1",
      idempotencyKey: "idem-publish-1",
      credentialReferenceId: unsafeOpaqueId<CredentialReferenceId>("credential-github-app"),
    });
    expect(client.lastPublishInput).not.toHaveProperty("workspace");
    expect(client.lastPublishInput).not.toHaveProperty("localRefs");
    expect(client.lastPublishInput).not.toHaveProperty("workingTree");
  });

  it("RT-025-B keeps publication evidence semantically scoped and replays idempotently", async () => {
    const client = new FakeGitHubClient();
    const publisher = new GitHubTrustedPublisher(client);

    const first = await publisher.publish(request());
    const replay = await publisher.publish(request());

    expect(client.publishCalls).toBe(1);
    expect(first.value.details).toMatchObject({
      targetRef: "refs/heads/awp/change-set-1",
      baseRevision: "base-a",
      candidateDigest: "candidate-tree-a",
    });
    expect(replay.references[0]?.nativeRevision).toBe("publication-commit-a");
  });

  it("RT-005/RT-040-B publishes from exact immutable identities, never workspace/log presentation state", async () => {
    const client = new FakeGitHubClient();
    await new GitHubTrustedPublisher(client).publish(request());

    expect(Object.keys(client.lastPublishInput ?? {}).sort()).toEqual(
      [
        "candidateDigest",
        "candidateManifest",
        "expectedBaseRevision",
        "diff",
        "changeSetId",
        "factoryRunId",
        "idempotencyKey",
        "credentialReferenceId",
        "repositoryKey",
        "targetRef",
      ].sort(),
    );
    expect(client.lastPublishInput).not.toHaveProperty("workspacePath");
    expect(client.lastPublishInput).not.toHaveProperty("refs");
    expect(client.lastPublishInput).not.toHaveProperty("log");
    expect(client.lastPublishInput).not.toHaveProperty("summary");
  });

  it("RT-006-GITHUB reconciles ambiguous publication success before retrying mutation", async () => {
    const client = new FakeGitHubClient();
    client.dropPublishResponseOnce = true;
    const publisher = new GitHubTrustedPublisher(client);

    await expect(publisher.publish(request())).rejects.toMatchObject({
      providerError: { category: "unavailable", retryable: true },
    });
    expect(client.publishCalls).toBe(1);
    expect(client.publication?.status).toBe("published");

    const retry = await publisher.publish(request());

    expect(client.publishCalls).toBe(1);
    expect(retry.value.state).toBe("published");
    expect(retry.references[0]?.nativeRevision).toBe("publication-commit-a");
  });

  it("reconciles an already-published replay even if repository head later advances", async () => {
    const client = new FakeGitHubClient();
    const publisher = new GitHubTrustedPublisher(client);
    await publisher.publish(request());
    client.repository = { ...client.repository, headRevision: "new-head" };

    const replay = await publisher.publish(request());

    expect(client.publishCalls).toBe(1);
    expect(replay.value.state).toBe("published");
    expect(replay.references[0]?.nativeRevision).toBe("publication-commit-a");
  });

  it("INV-FIRE-003 blocks publication when exact expected base head has moved", async () => {
    const client = new FakeGitHubClient();
    client.repository = { ...client.repository, headRevision: "new-head" };

    await expect(new GitHubTrustedPublisher(client).publish(request())).rejects.toMatchObject({
      providerError: { category: "conflict-stale", retryable: false },
    });
    expect(client.publishCalls).toBe(0);
  });

  it("INV-FIRE-015 rejects an idempotency replay with conflicting candidate fingerprint", async () => {
    const client = new FakeGitHubClient();
    client.publication = {
      nativeId: "publication-foreign",
      repositoryKey: "platform-modules/awp",
      targetRef: "refs/heads/awp/change-set-1",
      targetRevision: "other-tree",
      baseRevision: "base-a",
      candidateDigest: "other-tree",
      candidateManifest: { ...candidateManifest, treeDigest: "other-tree" },
      factoryRunId: "factory-run-1",
      idempotencyKey: "idem-publish-1",
      status: "published",
    };

    await expect(new GitHubTrustedPublisher(client).publish(request())).rejects.toMatchObject({
      providerError: { category: "conflict-stale" },
    });
  });

  it("RT-007 reconciles ambiguous trusted merge success before retrying mutation", async () => {
    const client = new FakeGitHubClient();
    const publisher = new GitHubTrustedPublisher(client);
    const publication = await publisher.publish(request());
    client.dropMergeResponseOnce = true;
    const merger = new GitHubTrustedMerger(client);
    const mergeRequest = {
      context: { ...context(), idempotencyKey: "idem-merge-1" },
      changeSetId: unsafeOpaqueId<ChangeSetId>("change-set-1"),
      repositoryKey: "platform-modules/awp",
      publicationReference: publication.references[0]!,
      candidateDigest: "candidate-tree-a",
      candidateManifest,
      expectedBase: { reference: "refs/heads/main", revision: "base-a" },
      expectedTarget: { reference: "refs/heads/main", revision: "base-a" },
    };

    await expect(merger.merge(mergeRequest)).rejects.toMatchObject({
      providerError: { category: "unavailable", retryable: true },
    });
    expect(client.mergeCalls).toBe(1);

    const reconciled = await merger.reconcile(mergeRequest);
    expect(reconciled.value).toMatchObject({
      state: "merged",
      result: { resultingRevision: "merge-revision-1" },
    });
    const replay = await merger.merge(mergeRequest);
    expect(client.mergeCalls).toBe(1);
    expect(replay.value.resultingRevision).toBe("merge-revision-1");
  });

  it("RT-040 passes the complete canonical candidate manifest to publication and merge", async () => {
    const client = new FakeGitHubClient();
    const publication = await new GitHubTrustedPublisher(client).publish(request());
    const merger = new GitHubTrustedMerger(client);
    await merger.merge({
      context: { ...context(), idempotencyKey: "idem-merge-manifest" },
      changeSetId: unsafeOpaqueId<ChangeSetId>("change-set-1"),
      repositoryKey: "platform-modules/awp",
      publicationReference: publication.references[0]!,
      candidateDigest: "candidate-tree-a",
      candidateManifest,
      expectedBase: { reference: "refs/heads/main", revision: "base-a" },
      expectedTarget: { reference: "refs/heads/main", revision: "base-a" },
    });
    expect(client.lastPublishInput).toMatchObject({ candidateManifest });
    expect(client.merge?.candidateManifest).toEqual(candidateManifest);
  });

  it("does not allow an AgentRun principal to invoke trusted publication", async () => {
    const client = new FakeGitHubClient();
    await expect(
      new GitHubTrustedPublisher(client).publish(request(context("agent"))),
    ).rejects.toBeInstanceOf(GitHubAdapterError);
    expect(client.publishCalls).toBe(0);
  });
});
