import { describe, expect, it, vi } from "vitest";
import {
	blobRawUrl,
	branches,
	gitTarget,
	log,
	resolveDefaultBranch,
	tag,
	tags,
	tree
} from "./gitclient";

const config = { bobbinUrl: "https://bobbin.example", knotMirrorUrl: "https://km.example" };
const repo = {
	uri: "at://did:plc:owner/sh.tangled.repo/core",
	repoDid: "did:plc:reporepo"
};

const jsonResponse = (body: unknown): Response =>
	new Response(JSON.stringify(body), {
		status: 200,
		headers: { "content-type": "application/json" }
	});

describe("gitTarget", () => {
	it("routes git ops to the knot mirror keyed by repo DID when set", () => {
		const target = gitTarget(config, repo, globalThis.fetch);
		expect(target.ctx.serviceUrl).toBe("https://km.example");
		expect(target.repo).toBe("did:plc:reporepo");
		expect(target.via).toBe("mirror");
	});

	it("uses bobbin keyed by at-uri without a mirror", () => {
		const target = gitTarget({ ...config, knotMirrorUrl: "" }, repo, globalThis.fetch);
		expect(target.ctx.serviceUrl).toBe("https://bobbin.example");
		expect(target.repo).toBe(repo.uri);
		expect(target.via).toBe("bobbin");
	});

	it("falls back to bobbin when the repo DID is unknown", () => {
		const target = gitTarget(config, { uri: repo.uri }, globalThis.fetch);
		expect(target.ctx.serviceUrl).toBe("https://bobbin.example");
		expect(target.repo).toBe(repo.uri);
		expect(target.via).toBe("bobbin");
	});
});

describe("resolveDefaultBranch", () => {
	it("reads the is_default flag out of the mirror's branch list", async () => {
		const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(
			jsonResponse({
				branches: [
					{ reference: { name: "sv-fe", hash: "aaa" } },
					{ reference: { name: "master", hash: "bbb" }, is_default: true }
				]
			})
		);
		const target = gitTarget(config, repo, fetchMock);

		await expect(resolveDefaultBranch(target)).resolves.toBe("master");

		const url = new URL(String(fetchMock.mock.calls[0][0]));
		expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.listBranches");
		expect(url.searchParams.get("repo")).toBe(repo.repoDid);
	});

	it("asks the knot endpoint directly on the bobbin path", async () => {
		const fetchMock = vi
			.fn<typeof globalThis.fetch>()
			.mockResolvedValue(jsonResponse({ name: "main", hash: "ccc", when: "" }));
		const target = gitTarget({ ...config, knotMirrorUrl: "" }, repo, fetchMock);

		await expect(resolveDefaultBranch(target)).resolves.toBe("main");

		const url = new URL(String(fetchMock.mock.calls[0][0]));
		expect(url.pathname).toBe("/xrpc/sh.tangled.repo.getDefaultBranch");
	});

	it("returns null instead of throwing when the backend is down", async () => {
		const fetchMock = vi
			.fn<typeof globalThis.fetch>()
			.mockRejectedValue(new TypeError("fetch failed"));
		const target = gitTarget(config, repo, fetchMock);

		await expect(resolveDefaultBranch(target)).resolves.toBeNull();
	});
});

describe("endpoint routing", () => {
	const mirrorTarget = (fetchMock: typeof globalThis.fetch) => gitTarget(config, repo, fetchMock);
	const bobbinTarget = (fetchMock: typeof globalThis.fetch) =>
		gitTarget({ ...config, knotMirrorUrl: "" }, repo, fetchMock);
	const calledUrl = (fetchMock: ReturnType<typeof vi.fn<typeof globalThis.fetch>>, call = 0) =>
		new URL(String(fetchMock.mock.calls[call][0]));

	it("sends log to the mirror's listCommits keyed by DID, cursor intact", async () => {
		const fetchMock = vi
			.fn<typeof globalThis.fetch>()
			.mockResolvedValue(jsonResponse({ commits: [] }));

		await log(mirrorTarget(fetchMock), { ref: "main", limit: 10, cursor: "abc" });

		const url = calledUrl(fetchMock);
		expect(url.origin).toBe("https://km.example");
		expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.listCommits");
		expect(url.searchParams.get("repo")).toBe(repo.repoDid);
		expect(url.searchParams.get("cursor")).toBe("abc");
	});

	it("sends log to bobbin's repo.log keyed by at-uri, cursor intact", async () => {
		const fetchMock = vi
			.fn<typeof globalThis.fetch>()
			.mockResolvedValue(jsonResponse({ commits: [] }));

		await log(bobbinTarget(fetchMock), { ref: "main", limit: 10, cursor: "abc" });

		const url = calledUrl(fetchMock);
		expect(url.origin).toBe("https://bobbin.example");
		expect(url.pathname).toBe("/xrpc/sh.tangled.repo.log");
		expect(url.searchParams.get("repo")).toBe(repo.uri);
		expect(url.searchParams.get("cursor")).toBe("abc");
	});

	it("sends tree to the mirror's getTree with the path param", async () => {
		const fetchMock = vi
			.fn<typeof globalThis.fetch>()
			.mockResolvedValue(jsonResponse({ files: [] }));

		await tree(mirrorTarget(fetchMock), { ref: "main", path: "src/lib" });

		const url = calledUrl(fetchMock);
		expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.getTree");
		expect(url.searchParams.get("repo")).toBe(repo.repoDid);
		expect(url.searchParams.get("path")).toBe("src/lib");
	});

	it("sends tree to bobbin's repo.tree", async () => {
		const fetchMock = vi
			.fn<typeof globalThis.fetch>()
			.mockResolvedValue(jsonResponse({ files: [] }));

		await tree(bobbinTarget(fetchMock), { ref: "main" });

		const url = calledUrl(fetchMock);
		expect(url.pathname).toBe("/xrpc/sh.tangled.repo.tree");
		expect(url.searchParams.get("repo")).toBe(repo.uri);
	});

	it("sends branches and tags to the legacy list endpoints on the mirror", async () => {
		const fetchMock = vi
			.fn<typeof globalThis.fetch>()
			.mockImplementation(() => Promise.resolve(jsonResponse({ branches: [], tags: [] })));
		const target = mirrorTarget(fetchMock);

		await branches(target, 5);
		await tags(target, 5);

		expect(calledUrl(fetchMock).pathname).toBe("/xrpc/sh.tangled.git.temp.listBranches");
		expect(calledUrl(fetchMock, 1).pathname).toBe("/xrpc/sh.tangled.git.temp.listTags");
	});

	it("routes single-tag reads to getTag on the mirror and repo.tag on bobbin", async () => {
		const fetchMock = vi
			.fn<typeof globalThis.fetch>()
			.mockImplementation(() => Promise.resolve(jsonResponse({ tag: undefined })));

		await tag(mirrorTarget(fetchMock), "v1.0.0");
		expect(calledUrl(fetchMock).pathname).toBe("/xrpc/sh.tangled.git.temp.getTag");

		await tag(bobbinTarget(fetchMock), "v1.0.0");
		expect(calledUrl(fetchMock, 1).pathname).toBe("/xrpc/sh.tangled.repo.tag");
	});

	it("builds the raw url against the mirror without a raw flag", () => {
		const url = new URL(
			blobRawUrl(mirrorTarget(globalThis.fetch), { ref: "main", path: "README.md" })
		);
		expect(url.origin).toBe("https://km.example");
		expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.getBlob");
		expect(url.searchParams.get("repo")).toBe(repo.repoDid);
		expect(url.searchParams.has("raw")).toBe(false);
	});

	it("builds the raw url against bobbin with raw=true", () => {
		const url = new URL(
			blobRawUrl(bobbinTarget(globalThis.fetch), { ref: "main", path: "README.md" })
		);
		expect(url.origin).toBe("https://bobbin.example");
		expect(url.pathname).toBe("/xrpc/sh.tangled.repo.blob");
		expect(url.searchParams.get("repo")).toBe(repo.uri);
		expect(url.searchParams.get("raw")).toBe("true");
	});
});
