import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
	alibabaUsageFeed,
	anthropicUsageFeed,
	antigravityUsageFeed,
	codexUsageFeed,
	createUsageState,
	deepseekUsageFeed,
	hyperUsageFeed,
	opencodegoUsageFeed,
	type SessionUsageCredential,
	type UsageCredential,
	type UsageResponse,
	type UsageStep,
	usageProviders,
	zaiUsageFeed,
} from "../src/index.js";

/**
 * The Zig core is a second implementation of the same rules, so it is checked against
 * the reference on the same bodies rather than on hand-written expectations. Both sides
 * are computed here at test time, which means a semantic change on either side fails
 * this test instead of silently drifting.
 */
const nativeDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "native", "usage");
const zig = process.env.ZIG ?? "zig";

function zigVersion(): string | undefined {
	try {
		return execFileSync(zig, ["version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
	} catch {
		return undefined;
	}
}

const version = zigVersion();

function advanceZig(
	provider: string,
	credential: UsageCredential,
	responses: UsageResponse[],
	nowMs: number = 0,
): UsageStep {
	const output = execFileSync(
		zig,
		[
			"build",
			"run",
			"--",
			"advance",
			provider,
			JSON.stringify(credential),
			JSON.stringify(responses),
			"--now-ms",
			String(nowMs),
		],
		{
			cwd: nativeDir,
			encoding: "utf8",
			stdio: ["ignore", "pipe", "pipe"],
		},
	)
		.trim()
		.split("\n")
		.at(-1) as string;

	return JSON.parse(output);
}

/**
 * The same call, but through several rounds in one process. The core owns no cross-call state, so
 * this is the only way to reach a feed whose later round depends on an earlier one: the CLI creates
 * one state and passes it back on each round, exactly as a native host does.
 */
function advanceZigRounds(
	provider: string,
	credential: UsageCredential,
	rounds: UsageResponse[][],
	nowMs: number = 0,
): UsageStep[] {
	const output = execFileSync(
		zig,
		[
			"build",
			"run",
			"--",
			"advance",
			provider,
			JSON.stringify(credential),
			...rounds.map((responses) => JSON.stringify(responses)),
			"--now-ms",
			String(nowMs),
		],
		{
			cwd: nativeDir,
			encoding: "utf8",
			stdio: ["ignore", "pipe", "pipe"],
		},
	).trim();

	return output.split("\n").map((line) => JSON.parse(line) as UsageStep);
}

/** The ABI carries headers as ordered pairs (HTTP allows a repeated name); TS carries a record. */
function pairsToRecord(pairs: [string, string][]): Record<string, string> {
	return Object.fromEntries(pairs);
}

describe.skipIf(version === undefined)("zig usage core parity", () => {
	it("reports the implemented providers and credential kinds matching their TS definitions", () => {
		const output = execFileSync(zig, ["build", "run", "--", "providers"], {
			cwd: nativeDir,
			encoding: "utf8",
		})
			.trim()
			.split("\n")
			.at(-1) as string;

		const zigProviders = JSON.parse(output) as {
			provider: string;
			providerName: string;
			credentialKinds: string[];
		}[];
		const tsAll = new Map(
			usageProviders().map((p) => [
				p.provider,
				{
					provider: p.provider,
					providerName: p.providerName,
					credentialKinds: [...p.credentialKinds],
				},
			]),
		);

		for (const zp of zigProviders) {
			expect(tsAll.get(zp.provider)).toEqual(zp);
		}
		expect(zigProviders.map((p) => p.provider)).toEqual(usageProviders().map((p) => p.provider));
	});

	it("codex-usage-live.json matches the TypeScript reference", () => {
		const nowMs = 1_789_514_000_000;
		const path = join(nativeDir, "tests", "fixtures", "codex-usage-live.json");
		const body = readFileSync(path, "utf8");
		const credential: UsageCredential = { kind: "oauth", access: "access-token", email: "user@example.com" };
		const responses: UsageResponse[] = [{ status: 200, body }];

		const tsStep = codexUsageFeed.advance(createUsageState(credential, nowMs), responses);
		const zigStep = advanceZig("openai-codex", credential, responses, nowMs);

		expect(zigStep).toEqual(tsStep);
	});

	it("codex-usage-edge.json matches the TypeScript reference", () => {
		const nowMs = 1_700_000_000_000;
		const path = join(nativeDir, "tests", "fixtures", "codex-usage-edge.json");
		const body = readFileSync(path, "utf8");
		const credential: UsageCredential = { kind: "oauth", access: "access-token", email: "user@example.com" };
		const responses: UsageResponse[] = [{ status: 200, body }];

		const tsStep = codexUsageFeed.advance(createUsageState(credential, nowMs), responses);
		const zigStep = advanceZig("openai-codex", credential, responses, nowMs);

		expect(zigStep).toEqual(tsStep);
	});

	it("anthropic usage matches the TypeScript reference", () => {
		const usageBody = readFileSync(join(nativeDir, "tests", "fixtures", "anthropic-usage.json"), "utf8");
		const profileBody = readFileSync(join(nativeDir, "tests", "fixtures", "anthropic-profile.json"), "utf8");
		const credential: UsageCredential = { kind: "oauth", access: "access-token" };
		const responses: UsageResponse[] = [
			{ status: 200, body: usageBody },
			{ status: 200, body: profileBody },
		];

		const tsStep = anthropicUsageFeed.advance(createUsageState(credential, 0), responses);
		const zigStep = advanceZig("anthropic", credential, responses, 0);

		expect(zigStep).toEqual(tsStep);
	});

	it("google-antigravity quota matches the TypeScript reference", () => {
		const nowMs = 1_789_514_000_000;
		const body = readFileSync(join(nativeDir, "tests", "fixtures", "antigravity-quota.json"), "utf8");
		const credential: UsageCredential = {
			kind: "oauth",
			access: "ya29.test-access-token",
			accountId: "aicode-consumers",
			email: "dev@example.com",
			extra: { projectId: "aicode-consumers" },
		};
		const responses: UsageResponse[] = [{ status: 200, body }];

		const tsStep = antigravityUsageFeed.advance(createUsageState(credential, nowMs), responses);
		const zigStep = advanceZig("google-antigravity", credential, responses, nowMs);

		expect(zigStep).toEqual(tsStep);
	});

	it("hyper billing matches the TypeScript reference", () => {
		const nowMs = 1_789_514_000_000;
		const billingBody = readFileSync(join(nativeDir, "tests", "fixtures", "hyper-billing.json"), "utf8");
		const membersBody = readFileSync(join(nativeDir, "tests", "fixtures", "hyper-members.json"), "utf8");
		const credential: UsageCredential = {
			kind: "apiKey",
			apiKey: "sk-hyper-test1234567890abcdef",
			accountId: "team_20439d8c54af",
		};
		const responses: UsageResponse[] = [
			{ status: 200, body: billingBody },
			{ status: 200, body: membersBody },
		];

		const tsStep = hyperUsageFeed.advance(createUsageState(credential, nowMs), responses);
		const zigStep = advanceZig("hyper", credential, responses, nowMs);

		expect(zigStep).toEqual(tsStep);
	});

	it("hyper hands the team from its teams round to its billing round, on both sides", () => {
		const nowMs = 1_789_514_000_000;
		// The team name carries a quote and a backslash because the note crosses a JSON boundary: a
		// writer that forgets to escape ends the string early, and this case stops parsing.
		const teamsBody = JSON.stringify({ items: [{ id: "team_20439d8c54af", name: 'Hyper "Squad" \\ back' }] });
		const billingBody = readFileSync(join(nativeDir, "tests", "fixtures", "hyper-billing.json"), "utf8");
		const membersBody = readFileSync(join(nativeDir, "tests", "fixtures", "hyper-members.json"), "utf8");
		const credential: UsageCredential = { kind: "apiKey", apiKey: "sk-hyper-test1234567890abcdef" };

		const rounds: UsageResponse[][] = [
			[{ status: 200, body: teamsBody }],
			[
				{ status: 200, body: billingBody },
				{ status: 200, body: membersBody },
			],
		];

		// One state, two rounds: the team is cached per credential in TS and in the host-owned state
		// on the Zig side, so a round that reads freed memory or a lost cache reports no team note.
		const state = createUsageState(credential, nowMs);
		const tsRounds = [hyperUsageFeed.advance(state, rounds[0]!), hyperUsageFeed.advance(state, rounds[1]!)];
		const zigRounds = advanceZigRounds("hyper", credential, rounds, nowMs);

		expect(zigRounds).toHaveLength(2);
		const [zigTeamsStep, zigBillingStep] = zigRounds as [
			{ requests: { method: string; url: string; headers: [string, string][] }[] },
			UsageStep,
		];
		const tsTeamsStep = tsRounds[0]!;
		if (!("requests" in tsTeamsStep)) throw new Error("round one should ask for teams");
		// The request shape is compared too — this is the one step where the two representations
		// differ on purpose (ordered pairs vs record).
		expect(zigTeamsStep.requests.map((r) => ({ ...r, headers: pairsToRecord(r.headers) }))).toEqual(
			tsTeamsStep.requests,
		);
		expect(zigBillingStep).toEqual(tsRounds[1]!);
	});

	it("a reset already in the past keeps the reference's window label", () => {
		const nowMs = 1_789_514_000_000;
		// Only next_at, so the label comes from epochToWindowLabel rather than the last/next difference,
		// and it is already behind now_ms: the reference floors that to "1h window".
		const billingBody = JSON.stringify({
			plan: { name: "Shred", type: "paid", hypercredits: 250 },
			status: "active",
			balances: { plan: { hypercredits: 237 }, total: { usd: "11.83" } },
			hypercredit_refresh: { next_at: new Date(nowMs - 3_600_000).toISOString() },
		});
		const credential: UsageCredential = {
			kind: "apiKey",
			apiKey: "sk-hyper-test1234567890abcdef",
			accountId: "team_20439d8c54af",
		};
		const responses: UsageResponse[] = [{ status: 200, body: billingBody }];

		const tsStep = hyperUsageFeed.advance(createUsageState(credential, nowMs), responses);
		const zigStep = advanceZig("hyper", credential, responses, nowMs);

		expect(zigStep).toEqual(tsStep);
	});

	it("opencode-go usage matches the TypeScript reference", () => {
		const body = readFileSync(join(nativeDir, "tests", "fixtures", "opencode-go-usage.json"), "utf8");
		const credential: UsageCredential = { kind: "apiKey", apiKey: "test-opencode-key" };
		const responses: UsageResponse[] = [{ status: 200, body }];

		const tsStep = opencodegoUsageFeed.advance(createUsageState(credential, 0), responses);
		const zigStep = advanceZig("opencode-go", credential, responses, 0);

		expect(zigStep).toEqual(tsStep);
	});

	it("zai usage matches the TypeScript reference", () => {
		const body = readFileSync(join(nativeDir, "tests", "fixtures", "zai-usage.json"), "utf8");
		const credential: UsageCredential = { kind: "apiKey", apiKey: "test-zai-key" };
		const responses: UsageResponse[] = [{ status: 200, body }];

		const tsStep = zaiUsageFeed.advance(createUsageState(credential, 0), responses);
		const zigStep = advanceZig("zai", credential, responses, 0);

		expect(zigStep).toEqual(tsStep);
	});

	it("deepseek balance matches the TypeScript reference", () => {
		const body = readFileSync(join(nativeDir, "tests", "fixtures", "deepseek-balance.json"), "utf8");
		const credential: UsageCredential = { kind: "apiKey", apiKey: "test-deepseek-key" };
		const responses: UsageResponse[] = [{ status: 200, body }];

		const tsStep = deepseekUsageFeed.advance(createUsageState(credential, 0), responses);
		const zigStep = advanceZig("deepseek", credential, responses, 0);

		expect(zigStep).toEqual(tsStep);
	});

	it("alibaba CLI first request matches the TypeScript reference", () => {
		const credential: UsageCredential = { kind: "cli", config: { region: "intl-personal" } };
		const tsStep = alibabaUsageFeed.advance(createUsageState(credential, 0), []);
		const zigStep = advanceZig("alibaba", credential, [], 0);

		expect(zigStep).toEqual(tsStep);
	});

	it("alibaba CLI answer matches the TypeScript reference", () => {
		const credential: UsageCredential = {
			kind: "cli",
			config: { region: "intl-personal" },
			email: "user@example.com",
		};
		const responses: UsageResponse[] = [
			{
				status: 0,
				body: JSON.stringify({
					per5HourPercentage: 0.5,
					per1WeekPercentage: 0.83603712139856,
					per1WeekResetTime: 1_790_021_160_000,
				}),
			},
		];

		const tsStep = alibabaUsageFeed.advance(createUsageState(credential, 1_700_000_000_000), responses);
		const zigStep = advanceZig("alibaba", credential, responses, 1_700_000_000_000);

		expect(zigStep).toEqual(tsStep);
	});

	it("alibaba console fallback matches the TypeScript reference", () => {
		const usageBody = readFileSync(join(nativeDir, "tests", "fixtures", "alibaba-usage.json"), "utf8");
		const subBody = readFileSync(join(nativeDir, "tests", "fixtures", "alibaba-subscription.json"), "utf8");
		const quotaBody = readFileSync(join(nativeDir, "tests", "fixtures", "alibaba-quota-config.json"), "utf8");
		const cardBody = readFileSync(join(nativeDir, "tests", "fixtures", "alibaba-reset-card_list.json"), "utf8");

		const credential: SessionUsageCredential = {
			kind: "session",
			session: {
				url: "https://bailian-singapore-cs.alibabacloud.com/data/api.json",
				region: "ap-southeast-1",
				secToken: "test-sec-token-123",
				headers: { cookie: "login_aliyunid=test; cna=test" },
			},
			email: "user@example.com",
		};
		const responses: UsageResponse[] = [
			{ status: 200, body: usageBody },
			{ status: 200, body: subBody },
			{ status: 200, body: quotaBody },
			{ status: 200, body: cardBody },
		];

		const tsStep = alibabaUsageFeed.advance(createUsageState(credential, 0), responses);
		const zigStep = advanceZig("alibaba", credential, responses, 0);

		expect(zigStep).toEqual(tsStep);
	});
});
