/**
 * Regression guard: the package registry must not serve private packages.
 *
 * The bug this locks down: `GET /npm/...` resolved a package by name and
 * returned its packument and tarball with no authorization check whatsoever.
 * The platform-wide private-repo gate in app.tsx keys on the owner/repo path
 * shape, which the npm protocol routes do not have (they address packages by
 * package name alone) and which the JSON helper routes under the api prefix
 * also miss. So `npm install` returned the FULL SOURCE of a private
 * repository's package to an anonymous caller.
 *
 * Two layers are asserted here:
 *   1. The decision rule in lib/package-access.ts, driven through its
 *      injectable-deps seam so no database is required.
 *   2. That the read routes actually still CALL that rule — a rule nothing
 *      invokes is worth nothing, and the original bug was a missing call.
 *
 * Deliberately no `mock.module`: bun loads every test module before running
 * any test, and mock.module swaps the registry entry rather than rebinding
 * namespaces an importer already holds, so mocking `../db` here would leak
 * into unrelated files in a full run.
 */

import { describe, it, expect, afterAll, beforeEach } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { Package } from "../db/schema";
import type { RepoAccessLevel } from "../middleware/repo-access";
import {
  canReadPackage,
  canReadRepoPackages,
  isRepoMember,
  __setPackageAccessDepsForTests,
} from "../lib/package-access";

const PUBLIC_REPO = { id: "repo-public", isPrivate: false };
const PRIVATE_REPO = { id: "repo-private", isPrivate: true };

const OWNER_ID = "user-owner";
const COLLAB_ID = "user-collab";
const STRANGER_ID = "user-stranger";

/**
 * Stand-in for the real resolver. Mirrors its contract: the owner and an
 * accepted collaborator get a level regardless of visibility; everyone else
 * falls back to "read" for public repos and "none" for private ones.
 */
async function fakeResolve(args: {
  repoId: string;
  userId: string | null;
  isPublic: boolean;
}): Promise<RepoAccessLevel> {
  if (args.userId === OWNER_ID) return "owner";
  if (args.userId === COLLAB_ID) return "read";
  return args.isPublic ? "read" : "none";
}

const REPOS_BY_ID: Record<string, { id: string; isPrivate: boolean }> = {
  [PUBLIC_REPO.id]: PUBLIC_REPO,
  [PRIVATE_REPO.id]: PRIVATE_REPO,
};

beforeEach(() => {
  __setPackageAccessDepsForTests({
    resolveRepoAccess: fakeResolve as any,
    loadRepoById: async (id: string) => REPOS_BY_ID[id] ?? null,
  });
});

afterAll(() => {
  __setPackageAccessDepsForTests(null);
});

function pkg(overrides: Partial<Package>): Package {
  return {
    id: "pkg-1",
    repositoryId: PUBLIC_REPO.id,
    ecosystem: "npm",
    scope: null,
    name: "widget",
    visibility: "public",
    ...overrides,
  } as Package;
}

describe("package read access — repository is private", () => {
  it("denies an anonymous caller", async () => {
    expect(await canReadRepoPackages(PRIVATE_REPO, null)).toBe(false);
  });

  it("denies a signed-in stranger", async () => {
    expect(await canReadRepoPackages(PRIVATE_REPO, STRANGER_ID)).toBe(false);
  });

  it("allows the repository owner", async () => {
    expect(await canReadRepoPackages(PRIVATE_REPO, OWNER_ID)).toBe(true);
  });

  it("allows an accepted collaborator", async () => {
    expect(await canReadRepoPackages(PRIVATE_REPO, COLLAB_ID)).toBe(true);
  });
});

describe("package read access — repository is public", () => {
  it("allows an anonymous caller", async () => {
    expect(await canReadRepoPackages(PUBLIC_REPO, null)).toBe(true);
  });

  it("still denies a package explicitly marked private", async () => {
    // visibility is stamped at publish time and never rewritten, so a
    // package marked private must stay private even in a public repo.
    expect(await canReadRepoPackages(PUBLIC_REPO, null, "private")).toBe(
      false
    );
    expect(await canReadRepoPackages(PUBLIC_REPO, STRANGER_ID, "private")).toBe(
      false
    );
  });

  it("allows members to see an explicitly private package", async () => {
    expect(await canReadRepoPackages(PUBLIC_REPO, OWNER_ID, "private")).toBe(
      true
    );
    expect(await canReadRepoPackages(PUBLIC_REPO, COLLAB_ID, "private")).toBe(
      true
    );
  });
});

describe("canReadPackage resolves the owning repository live", () => {
  it("denies an anonymous caller a package in a private repo", async () => {
    // The package row still says "public" because it was published while the
    // repo was public; the LIVE repo row is what must decide.
    const stale = pkg({ repositoryId: PRIVATE_REPO.id, visibility: "public" });
    expect(await canReadPackage(stale, null)).toBe(false);
  });

  it("allows the owner the same package", async () => {
    const stale = pkg({ repositoryId: PRIVATE_REPO.id, visibility: "public" });
    expect(await canReadPackage(stale, OWNER_ID)).toBe(true);
  });

  it("allows an anonymous caller a public package in a public repo", async () => {
    expect(await canReadPackage(pkg({}), null)).toBe(true);
  });

  it("denies when the owning repository no longer exists", async () => {
    const dangling = pkg({ repositoryId: "repo-deleted" });
    expect(await canReadPackage(dangling, OWNER_ID)).toBe(false);
  });
});

describe("isRepoMember(", () => {
  it("is false for anonymous even on a public repo", async () => {
    // Membership is not the same as readability — a public reader is not a
    // member, which is what decides visibility of private packages.
    expect(await isRepoMember(PUBLIC_REPO.id, null)).toBe(false);
  });

  it("is false for a signed-in stranger", async () => {
    expect(await isRepoMember(PUBLIC_REPO.id, STRANGER_ID)).toBe(false);
  });

  it("is true for the owner and for a collaborator", async () => {
    expect(await isRepoMember(PUBLIC_REPO.id, OWNER_ID)).toBe(true);
    expect(await isRepoMember(PUBLIC_REPO.id, COLLAB_ID)).toBe(true);
  });
});

// --- call sites stay wired -------------------------------------------------
//
// Strip ONLY line comments before asserting. A block-comment regex eats route
// paths (they contain a slash followed by a star), and these files are full
// of them.

function sourceWithoutLineComments(relPath: string): string {
  const text = readFileSync(join(import.meta.dir, "..", relPath), "utf8");
  const stripped = text
    .split("\n")
    .filter((l) => !l.trim().startsWith("//"))
    .join("\n");
  expect(stripped.length).toBeGreaterThan(0);
  return stripped;
}

/** Slice by anchor, never by character count — handlers move. */
function handlerBody(src: string, startAnchor: string, endAnchor: string) {
  const start = src.indexOf(startAnchor);
  expect(start).toBeGreaterThan(-1);
  const end = src.indexOf(endAnchor, start + startAnchor.length);
  expect(end).toBeGreaterThan(start);
  const body = src.slice(start, end);
  expect(body.length).toBeGreaterThan(0);
  return body;
}

describe("registry read routes invoke the access gate", () => {
  it("GET /npm/ checks canReadPackage before serving", async () => {
    const src = sourceWithoutLineComments("routes/packages-api.ts");
    const body = handlerBody(src, `api.get("/npm/*"`, `api.put("/npm/*"`);
    expect(body).toContain("canReadPackage(");
    // The tarball branch lives inside this handler, so gating the handler
    // gates the tarball too — assert the gate precedes the tarball read.
    const gateAt = body.indexOf("canReadPackage(");
    const tarballAt = body.indexOf("match.tarball");
    expect(tarballAt).toBeGreaterThan(gateAt);
  });

  it("the JSON package routes check the gate", async () => {
    const src = sourceWithoutLineComments("routes/packages-api.ts");
    const listBody = handlerBody(
      src,
      `api.get("/api/packages/:owner/:repo"`,
      `api.get("/api/packages/:owner/:repo/:pkgName`
    );
    expect(listBody).toContain("canReadRepoPackages(");
    expect(listBody).toContain("isRepoMember(");

    const detailBody = handlerBody(
      src,
      `api.get("/api/packages/:owner/:repo/:pkgName`,
      `api.get("/npm/*"`
    );
    expect(detailBody).toContain("canReadRepoPackages(");
  });

  it("the package UI routes filter private packages for non-members", async () => {
    const src = sourceWithoutLineComments("routes/packages.tsx");
    const listBody = handlerBody(
      src,
      `ui.get("/:owner/:repo/packages"`,
      `ui.get("/:owner/:repo/packages/:pkgName`
    );
    expect(listBody).toContain("isRepoMember(");
    // The rendered rows must come from the filtered list, not the raw query.
    expect(listBody).toContain("visiblePkgs.map");
    expect(listBody).not.toContain("rows = pkgs.map");

    const detailBody = handlerBody(
      src,
      `ui.get("/:owner/:repo/packages/:pkgName`,
      "export default"
    );
    expect(detailBody).toContain("isRepoMember(");
  });
});
