import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import { unsafeOpaqueId, type CredentialReferenceId } from "@awp/contracts";
import {
  GitHubHttpError,
  GitHubTrustedHttpClient,
  parseGitHubRepositoryUrl,
  type GitHubCredentialResolver,
  type GitHubFetch,
} from "../../../packages/providers/vcs-github/src/http-client.js";

const credentialReferenceId = unsafeOpaqueId<CredentialReferenceId>("credential:github-http-test");
const baseRevision = "1".repeat(40);
const candidateDigest = "2".repeat(40);
const publicationCommit = "3".repeat(40);
const mergeCommit = "4".repeat(40);
const otherTarget = "5".repeat(40);
const diff =
  "diff --git a/README.md b/README.md\n--- a/README.md\n+++ b/README.md\n@@ -1 +1 @@\n-old\n+new\n";
const candidateManifest = {
  treeDigest: candidateDigest,
  patchDigest: createHash("sha256").update(diff).digest("hex"),
  changedPaths: ["README.md"],
  changes: [{ path: "README.md", kind: "modify" as const }],
};

function resolver(observed: string[]): GitHubCredentialResolver {
  return {
    async resolve(referenceId, purpose) {
      expect(referenceId).toBe(credentialReferenceId);
      observed.push(purpose);
      return { token: "test-token", dispose() {} };
    },
  };
}

function response(status: number, body?: unknown) {
  const text = body === undefined ? "" : JSON.stringify(body);
  return {
    ok: status >= 200 && status < 300,
    status,
    statusText: String(status),
    async text() {
      return text;
    },
  };
}

class StatefulGitHubApi {
  readonly calls: { method: string; url: string; body?: unknown; authorization?: string }[] = [];
  publicationMessage = "";
  publicationRef?: string;
  mainRef = baseRevision;
  mergePosts = 0;
  refPosts = 0;

  readonly fetch: GitHubFetch = async (url, init) => {
    const body = init.body ? (JSON.parse(init.body) as unknown) : undefined;
    this.calls.push({
      method: init.method,
      url,
      ...(body === undefined ? {} : { body }),
      authorization: init.headers.Authorization,
    });
    const path = new URL(url).pathname + new URL(url).search;

    if (init.method === "GET" && path === "/repos/platform-modules/awp") {
      return response(200, {
        id: 42,
        default_branch: "main",
        html_url: "https://github.com/platform-modules/awp",
      });
    }
    if (init.method === "GET" && path === "/repos/platform-modules/awp/git/ref/heads/main") {
      return response(200, { object: { sha: this.mainRef } });
    }
    if (
      init.method === "GET" &&
      path === "/repos/platform-modules/awp/git/ref/heads/awp/change-set-http"
    ) {
      return this.publicationRef
        ? response(200, { object: { sha: this.publicationRef } })
        : response(404, { message: "Not Found" });
    }
    if (
      init.method === "GET" &&
      path === `/repos/platform-modules/awp/git/commits/${baseRevision}`
    ) {
      return response(200, { tree: { sha: "base-tree" }, parents: [] });
    }
    if (
      init.method === "GET" &&
      path === "/repos/platform-modules/awp/git/trees/base-tree?recursive=1"
    ) {
      return response(200, {
        truncated: false,
        tree: [{ path: "README.md", mode: "100644", type: "blob", sha: "base-blob" }],
      });
    }
    if (init.method === "GET" && path === "/repos/platform-modules/awp/git/blobs/base-blob") {
      return response(200, {
        encoding: "base64",
        content: Buffer.from("old\n").toString("base64"),
      });
    }
    if (init.method === "POST" && path === "/repos/platform-modules/awp/git/blobs") {
      return response(201, { sha: "candidate-blob" });
    }
    if (init.method === "POST" && path === "/repos/platform-modules/awp/git/trees") {
      expect(body).toMatchObject({ base_tree: "base-tree" });
      return response(201, { sha: candidateDigest });
    }
    if (init.method === "POST" && path === "/repos/platform-modules/awp/git/commits") {
      const object = body as Record<string, unknown>;
      expect(object.tree).toBe(candidateDigest);
      expect(object.parents).toEqual([baseRevision]);
      this.publicationMessage = String(object.message);
      return response(201, { sha: publicationCommit });
    }
    if (init.method === "POST" && path === "/repos/platform-modules/awp/git/refs") {
      this.refPosts += 1;
      expect(body).toMatchObject({
        ref: "refs/heads/awp/change-set-http",
        sha: publicationCommit,
      });
      this.publicationRef = publicationCommit;
      return response(201, { ref: "refs/heads/awp/change-set-http" });
    }
    if (
      init.method === "GET" &&
      path === `/repos/platform-modules/awp/git/commits/${publicationCommit}`
    ) {
      return response(200, {
        message: this.publicationMessage,
        tree: { sha: candidateDigest },
        parents: [{ sha: baseRevision }],
      });
    }
    if (
      init.method === "GET" &&
      path.startsWith(`/repos/platform-modules/awp/compare/${publicationCommit}...`)
    ) {
      return response(200, {
        status: this.mainRef === mergeCommit ? "ahead" : "diverged",
      });
    }
    if (init.method === "POST" && path === "/repos/platform-modules/awp/merges") {
      this.mergePosts += 1;
      expect(body).toMatchObject({ base: "main", head: publicationCommit });
      this.mainRef = mergeCommit;
      return response(201, { sha: mergeCommit });
    }
    if (
      init.method === "GET" &&
      path === `/repos/platform-modules/awp/git/commits/${mergeCommit}`
    ) {
      return response(200, { tree: { sha: candidateDigest }, parents: [{ sha: baseRevision }] });
    }
    throw new Error(`Unexpected fake GitHub request: ${init.method} ${path}`);
  };
}

async function publish(client: GitHubTrustedHttpClient) {
  return client.publishCandidate({
    repositoryKey: "platform-modules/awp",
    targetRef: "refs/heads/awp/change-set-http",
    expectedBaseRevision: baseRevision,
    candidateDigest,
    candidateManifest,
    diff,
    changeSetId: "change-set-http",
    factoryRunId: "factory-run-http",
    idempotencyKey: "idem:http:publish",
    credentialReferenceId,
  });
}

describe("GitHub repository HTTP boundary", () => {
  it("canonicalizes GitHub HTTPS/SSH repository identity and rejects ambiguous authority", () => {
    expect(parseGitHubRepositoryUrl("https://github.com/platform-modules/awp.git")).toEqual({
      owner: "platform-modules",
      repository: "awp",
      repositoryKey: "platform-modules/awp",
    });
    expect(parseGitHubRepositoryUrl("git@github.com:platform-modules/awp.git").repositoryKey).toBe(
      "platform-modules/awp",
    );
    expect(() => parseGitHubRepositoryUrl("https://example.com/platform-modules/awp.git")).toThrow(
      /canonical credential-free GitHub/,
    );
    expect(() =>
      parseGitHubRepositoryUrl("https://token@github.com/platform-modules/awp.git"),
    ).toThrow(/canonical credential-free GitHub/);
    expect(() =>
      parseGitHubRepositoryUrl("https://github.com/platform-modules/awp/issues"),
    ).toThrow(/exactly owner\/repository/);
  });

  it("inspects the configured repository using only the credential-reference resolver", async () => {
    const purposes: string[] = [];
    const api = new StatefulGitHubApi();
    const client = new GitHubTrustedHttpClient(resolver(purposes), api.fetch);
    const repository = await client.inspectRepository(
      "platform-modules/awp",
      credentialReferenceId,
    );

    expect(repository).toMatchObject({
      repositoryKey: "platform-modules/awp",
      defaultBranch: "main",
      headRevision: baseRevision,
    });
    expect(purposes).toEqual(["vcs.inspect", "vcs.ref.read"]);
    expect(api.calls.every((call) => call.authorization === "Bearer test-token")).toBe(true);
  });

  it("publishes one exact candidate branch, reads it back, and reconciles replay without a second mutation", async () => {
    const purposes: string[] = [];
    const api = new StatefulGitHubApi();
    const client = new GitHubTrustedHttpClient(resolver(purposes), api.fetch);

    const first = await publish(client);
    const replay = await publish(client);

    expect(first).toMatchObject({
      repositoryKey: "platform-modules/awp",
      targetRef: "refs/heads/awp/change-set-http",
      targetRevision: publicationCommit,
      baseRevision,
      candidateDigest,
      factoryRunId: "factory-run-http",
      idempotencyKey: "idem:http:publish",
      status: "published",
    });
    expect(first.candidateManifest).toEqual(candidateManifest);
    expect(api.publicationMessage).toContain("AWP-ChangeSet: change-set-http");
    expect(api.publicationMessage).toContain("AWP-FactoryRun: factory-run-http");
    expect(replay.targetRevision).toBe(publicationCommit);
    expect(api.refPosts).toBe(1);
  });

  it("merges the exact published candidate once, reconciles replay, and rejects a moved target", async () => {
    const api = new StatefulGitHubApi();
    const client = new GitHubTrustedHttpClient(resolver([]), api.fetch);
    const publication = await publish(client);
    const mergeInput = {
      repositoryKey: "platform-modules/awp",
      targetRef: "refs/heads/main",
      expectedTargetRevision: baseRevision,
      expectedBaseRevision: baseRevision,
      publicationNativeId: publication.nativeId,
      candidateDigest,
      candidateManifest,
      idempotencyKey: "idem:http:merge",
      credentialReferenceId,
    };

    const first = await client.mergePublication(mergeInput);
    const replay = await client.mergePublication(mergeInput);

    expect(first).toMatchObject({ resultingRevision: mergeCommit, status: "merged" });
    expect(replay.resultingRevision).toBe(mergeCommit);
    expect(api.mergePosts).toBe(1);

    const movedApi = new StatefulGitHubApi();
    const movedClient = new GitHubTrustedHttpClient(resolver([]), movedApi.fetch);
    const movedPublication = await publish(movedClient);
    movedApi.mainRef = otherTarget;
    await expect(
      movedClient.mergePublication({
        ...mergeInput,
        publicationNativeId: movedPublication.nativeId,
      }),
    ).rejects.toBeInstanceOf(GitHubHttpError);
    expect(movedApi.mergePosts).toBe(0);
  });
});
