import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it, vi } from "vitest";
import {
	anthropicUsageFeed,
	codexUsageFeed,
	collectUsage,
	createUsageState,
	type FetchLike,
	httpRequest,
	registerUsageFeed,
	runUsageFeed,
	type UsageCredential,
	unregisterUsageFeed,
	usageProviders,
} from "../src/index.js";

const fixtures = resolve(
	dirname(fileURLToPath(import.meta.url)),
	"..",
	"..",
	"..",
	"native",
	"usage",
	"tests",
	"fixtures",
);
const fixture = (name: string): unknown => JSON.parse(readFileSync(join(fixtures, name), "utf8"));

const codexCredential: UsageCredential = { kind: "oauth", access: "access-token", email: "codex@example.com" };
const anthropicCredential: UsageCredential = { kind: "oauth", access: "access-token" };

/** A host that answers with queued bodies instead of the network. */
function scriptedFetch(responses: { status: number; body: string }[]): FetchLike {
	const queue = [...responses];
	return async () => {
		const next = queue.shift();
		if (!next) throw new Error("scripted fetch ran out of responses");
		return new Response(next.body, { status: next.status });
	};
}

describe("usage feeds", () => {
	it("registers the shipped providers under one lookup", () => {
		const providers = usageProviders().map((feed) => feed.provider);
		expect(providers).toContain("openai-codex");
		expect(providers).toContain("anthropic");
	});

	it("asks for one request, then reports from the response", async () => {
		const state = createUsageState(codexCredential, 1_789_514_000_000);
		const first = codexUsageFeed.advance(state, []);
		expect("requests" in first && first.requests).toHaveLength(1);
		if (!("requests" in first)) throw new Error("expected requests");
		const request = httpRequest(first.requests[0]!);
		expect(request.url).toBe("https://chatgpt.com/backend-api/wham/usage");
		expect(request.headers?.Authorization).toBe("Bearer access-token");

		const step = codexUsageFeed.advance({ ...state, responses: [] }, [
			{ status: 200, body: JSON.stringify(fixture("codex-usage-live.json")) },
		]);
		if (!("report" in step)) throw new Error("expected a report");
		expect(step.report.planType).toBe("prolite");
		expect(step.report.limits).toHaveLength(3);
		expect(step.report.limits[0]).toMatchObject({ label: "Primary", usedPercent: 100, status: "exhausted" });
	});

	it("sends the account header only when the account is known", () => {
		const withoutAccount = codexUsageFeed.advance(createUsageState(codexCredential, 0), []);
		expect("requests" in withoutAccount && httpRequest(withoutAccount.requests[0]!).headers).not.toHaveProperty(
			"ChatGPT-Account-Id",
		);

		const withAccount = codexUsageFeed.advance(createUsageState({ ...codexCredential, accountId: "acct_1" }, 0), []);
		expect("requests" in withAccount && httpRequest(withAccount.requests[0]!).headers?.["ChatGPT-Account-Id"]).toBe(
			"acct_1",
		);
	});

	it("runs the host loop against a scripted host", async () => {
		const report = await runUsageFeed(codexUsageFeed, codexCredential, {
			nowMs: 1_789_514_000_000,
			fetchImpl: scriptedFetch([{ status: 200, body: JSON.stringify(fixture("codex-usage-live.json")) }]),
		});
		expect(report.provider).toBe("openai-codex");
		expect(report.providerName).toBe("ChatGPT Codex");
		expect(report.error).toBeNull();
	});

	it("reports a failed fetch as data, not as an exception", async () => {
		const report = await runUsageFeed(codexUsageFeed, codexCredential, {
			fetchImpl: scriptedFetch([{ status: 500, body: "nope" }]),
		});
		expect(report.error).toBe("HTTP 500");
		expect(report.limits).toEqual([]);
	});

	it("stops after the round limit when a feed never finishes", async () => {
		registerUsageFeed({
			provider: "looper",
			providerName: "Looper",
			credentialKinds: ["apiKey"],
			advance: () => ({ requests: [{ kind: "http", method: "GET", url: "https://example.invalid/usage" }] }),
		});
		try {
			const report = await runUsageFeed(
				{
					provider: "looper",
					providerName: "Looper",
					credentialKinds: ["apiKey"],
					advance: () => ({ requests: [{ kind: "http", method: "GET", url: "https://example.invalid/usage" }] }),
				},
				{ kind: "apiKey", apiKey: "k" },
				{ fetchImpl: scriptedFetch(Array.from({ length: 4 }, () => ({ status: 200, body: "{}" }))) },
			);
			expect(report.error).toMatch(/did not finish/);
		} finally {
			unregisterUsageFeed("looper");
		}
	});
});

describe("anthropic feed", () => {
	const responses = [
		{ status: 200, body: JSON.stringify(fixture("anthropic-usage.json")) },
		{ status: 200, body: JSON.stringify(fixture("anthropic-profile.json")) },
	];

	it("uses the normalised limits array instead of the named windows", () => {
		const step = anthropicUsageFeed.advance(createUsageState(anthropicCredential, 0), []);
		if (!("requests" in step)) throw new Error("expected requests");
		expect(step.requests.map((request) => httpRequest(request).url)).toEqual([
			"https://api.anthropic.com/api/oauth/usage",
			"https://api.anthropic.com/api/oauth/profile",
		]);
		expect(httpRequest(step.requests[0]!).headers?.["anthropic-beta"]).toBe("oauth-2025-04-20");
	});

	it("maps percent, ISO reset times and severity onto the contract", () => {
		const step = anthropicUsageFeed.advance(createUsageState(anthropicCredential, 0), responses);
		if (!("report" in step)) throw new Error("expected a report");
		expect(step.report.limits.map((limit) => limit.label)).toEqual(["Session", "Weekly"]);
		const weekly = step.report.limits[1]!;
		expect(weekly).toMatchObject({ usedPercent: 94, windowLabel: "7d window", status: "warning" });
		expect(weekly.resetsAt).toBe(Date.parse("2026-09-16T08:00:00.037593+00:00"));
		expect(step.report.planType).toBe("team_standard");
		expect(step.report.email).toBe("user@example.com");
	});

	it("keeps a window with no percentage as unknown rather than zero", () => {
		expect(codexUsageFeed).toBeDefined();
		const step = anthropicUsageFeed.advance(createUsageState(anthropicCredential, 0), [
			{
				status: 200,
				body: JSON.stringify({
					limits: [{ kind: "session", group: "session", percent: null, severity: "normal" }],
				}),
			},
			{ status: 200, body: "{}" },
		]);
		if (!("report" in step)) throw new Error("expected a report");
		expect(step.report.limits[0]).toEqual({ label: "Session", windowLabel: "5h window", status: "unknown" });
	});

	it("surfaces extra usage when it is on and notes when it is off", () => {
		const on = anthropicUsageFeed.advance(createUsageState(anthropicCredential, 0), [
			{
				status: 200,
				body: JSON.stringify({
					limits: [],
					extra_usage: { is_enabled: true, monthly_limit: 100, used_credits: 25 },
				}),
			},
			{ status: 200, body: "{}" },
		]);
		if (!("report" in on)) throw new Error("expected a report");
		expect(on.report.limits[0]).toMatchObject({ label: "Extra usage", usedPercent: 25 });

		const off = anthropicUsageFeed.advance(createUsageState(anthropicCredential, 0), [
			{
				status: 200,
				body: JSON.stringify({ limits: [], extra_usage: { is_enabled: false, credits_ever_enabled: true } }),
			},
			{ status: 200, body: "{}" },
		]);
		if (!("report" in off)) throw new Error("expected a report");
		expect(off.report.notes).toEqual(["Extra usage is off"]);
	});
});

describe("collectUsage", () => {
	it("names a provider with no feed instead of dropping it", async () => {
		const result = await collectUsage([{ provider: "google", credential: { kind: "apiKey", apiKey: "key" } }], {
			fetchImpl: vi.fn() as unknown as FetchLike,
		});
		expect(result.reports).toEqual([]);
		expect(result.skipped).toEqual([{ provider: "google", reason: "no usage feed" }]);
	});

	it("says which credential a provider needs when the host cannot supply it", async () => {
		const result = await collectUsage([{ provider: "alibaba", credential: { kind: "apiKey", apiKey: "sk-sp-x" } }], {
			fetchImpl: vi.fn() as unknown as FetchLike,
		});
		expect(result.reports).toEqual([]);
		expect(result.skipped).toEqual([{ provider: "alibaba", reason: "needs a cli or session credential" }]);
	});

	it("explains a credential kind the feed cannot use", async () => {
		const result = await collectUsage([{ provider: "openai-codex", credential: { kind: "apiKey", apiKey: "k" } }]);
		expect(result.skipped[0]).toEqual({ provider: "openai-codex", reason: "needs a oauth credential" });
	});

	it("collects a report per credential", async () => {
		const result = await collectUsage(
			[
				{ provider: "openai-codex", credential: codexCredential },
				{ provider: "anthropic", credential: anthropicCredential },
			],
			{
				nowMs: 1_789_514_000_000,
				fetchImpl: scriptedFetch([
					{ status: 200, body: JSON.stringify(fixture("codex-usage-live.json")) },
					{ status: 200, body: JSON.stringify(fixture("anthropic-usage.json")) },
					{ status: 200, body: JSON.stringify(fixture("anthropic-profile.json")) },
				]),
			},
		);
		expect(result.skipped).toEqual([]);
		expect(result.reports.map((report) => report.provider)).toEqual(["openai-codex", "anthropic"]);
	});
});
