import { describe, expect, it, vi, type Mock } from "vitest";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import { createAppviewClient } from "./appview";
import { ClientResponseError } from "./client";
import { searchCode } from "./search";

const NSID = "org.tangled.temp.search.searchCode";
const API_URL = "http://127.0.0.1:3000";

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

// the agent is only ever asked for a service-auth token, so a stub that answers
// getServiceAuth is enough, and its call args are where the minted claims are.
const makeAgent = (token = "jwt-1") => {
	const handle = vi
		.fn<(pathname: string, init: RequestInit) => Promise<Response>>()
		.mockImplementation(() => Promise.resolve(jsonResponse({ token })));
	return { agent: { handle, sub: "did:plc:tester" } as unknown as OAuthUserAgent, handle };
};

const mintedParams = (handle: Mock): URLSearchParams =>
	new URL(String(handle.mock.calls[0][0]), "https://pds.test").searchParams;

const makeCtx = (fetchMock: typeof globalThis.fetch, token = "jwt-1") => {
	const { agent, handle } = makeAgent(token);
	return { ctx: createAppviewClient({ apiUrl: API_URL, agent, fetch: fetchMock }), handle };
};

describe("searchCode", () => {
	// the appview reply is discarded until the lexicon carries the fields
	// CodeResultCard needs, so a failing call is the only thing observable from
	// outside. it has to reject rather than fall back to the fake results.
	it("rejects when the appview fails, rather than resolving with fake results", async () => {
		const fetchMock = vi
			.fn<typeof globalThis.fetch>()
			.mockResolvedValue(
				jsonResponse(
					{ error: "MethodNotImplemented", message: "code search is not configured" },
					{ status: 501 }
				)
			);
		const { ctx } = makeCtx(fetchMock);

		await expect(searchCode(ctx, { q: "main" })).rejects.toThrow(ClientResponseError);
	});

	it("calls the configured appview at the searchCode nsid", async () => {
		const fetchMock = vi
			.fn<typeof globalThis.fetch>()
			.mockResolvedValue(jsonResponse({ results: [] }));
		const { ctx } = makeCtx(fetchMock);

		await searchCode(ctx, { q: "main", limit: 50 });

		const url = new URL(String(fetchMock.mock.calls[0][0]));
		expect(url.origin).toBe(API_URL);
		expect(url.pathname).toBe(`/xrpc/${NSID}`);
		expect(url.searchParams.get("q")).toBe("main");
		expect(url.searchParams.get("limit")).toBe("50");
	});

	// the aud has to name the appview, or VerifyServiceAuth rejects with a 403.
	// deriving it from apiUrl is what keeps it from drifting off the target host.
	it("mints a token audienced at the appview host, scoped to the method", async () => {
		const fetchMock = vi
			.fn<typeof globalThis.fetch>()
			.mockResolvedValue(jsonResponse({ results: [] }));
		const { ctx, handle } = makeCtx(fetchMock, "jwt-abc");

		await searchCode(ctx, { q: "main" });

		const minted = mintedParams(handle);
		expect(minted.get("aud")).toBe("did:web:127.0.0.1%3A3000");
		expect(minted.get("lxm")).toBe(NSID);

		const headers = new Headers((fetchMock.mock.calls[0][1] as RequestInit | undefined)?.headers);
		expect(headers.get("authorization")).toBe("Bearer jwt-abc");
	});
});
