import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
	antigravityUsageFeed,
	createUsageState,
	type FetchLike,
	httpRequest,
	hyperUsageFeed,
	runUsageFeed,
	type UsageCredential,
} from "../src/index.js";
import { parseAntigravityUsage } from "../src/utils/usage/providers/antigravity.js";
import { parseHyperBilling } from "../src/utils/usage/providers/hyper.js";

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

const antigravityOauthCredential: UsageCredential = {
	kind: "oauth",
	access: "ya29.test-access-token",
	accountId: "aicode-consumers",
	email: "dev@example.com",
	extra: {
		projectId: "aicode-consumers",
	},
};

const hyperOauthCredential: UsageCredential = {
	kind: "oauth",
	access: "hyper-oauth-access-token",
	email: "user@example.com",
};

const hyperApiKeyCredential: UsageCredential = {
	kind: "apiKey",
	apiKey: "sk-hyper-test1234567890abcdef",
	accountId: "team_20439d8c54af",
};

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("Google Antigravity usage feed", () => {
	it("requires an OAuth credential and rejects apiKey or session", () => {
		const stepApiKey = antigravityUsageFeed.advance(createUsageState({ kind: "apiKey", apiKey: "secret" }, 0), []);
		if (!("report" in stepApiKey)) throw new Error("expected a report");
		expect(stepApiKey.report.error).toMatch(/needs an OAuth account/);

		const stepSession = antigravityUsageFeed.advance(createUsageState({ kind: "session", session: {} }, 0), []);
		if (!("report" in stepSession)) throw new Error("expected a report");
		expect(stepSession.report.error).toMatch(/needs an OAuth account/);
	});

	it("sends a POST request with required antigravity User-Agent and empty body", () => {
		const state = createUsageState(antigravityOauthCredential, 1_789_514_000_000);
		const step = antigravityUsageFeed.advance(state, []);
		if (!("requests" in step)) throw new Error("expected requests");
		expect(step.requests).toHaveLength(1);
		const req = httpRequest(step.requests[0]!);
		expect(req.method).toBe("POST");
		expect(req.url).toBe("https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota");
		expect(req.headers?.Authorization).toBe("Bearer ya29.test-access-token");
		expect(req.headers?.["Content-Type"]).toBe("application/json");
		expect(req.headers?.["User-Agent"]).toMatch(/^antigravity\//);
		expect(req.body).toBe("{}");
	});

	it("pools 33 buckets into Gemini, Claude, and GPT-OSS families while dropping chat/tab models", () => {
		const raw = fixture("antigravity-quota.json");
		const { limits } = parseAntigravityUsage(raw, 1_789_514_000_000);

		// Expect 3 families in order
		expect(limits.map((l) => l.label)).toEqual(["Gemini", "Claude", "GPT-OSS"]);

		// Gemini: 0.16592255 remaining -> 83.4% used, 17% free
		const gemini = limits[0]!;
		expect(gemini.usedPercent).toBe(83.4);
		expect(gemini.detail).toBe("17% free");
		expect(gemini.resetsAt).toBe(Date.parse("2026-09-15T23:46:00Z"));
		expect(gemini.status).toBe("ok");

		// Claude: 1 remaining -> 0.0% used, 100% free
		const claude = limits[1]!;
		expect(claude.usedPercent).toBe(0);
		expect(claude.detail).toBe("100% free");
		expect(claude.resetsAt).toBe(Date.parse("2026-09-16T04:40:57Z"));
		expect(claude.status).toBe("ok");

		// GPT-OSS: 1 remaining -> 0.0% used, 100% free
		const gpt = limits[2]!;
		expect(gpt.usedPercent).toBe(0);
		expect(gpt.detail).toBe("100% free");
		expect(gpt.resetsAt).toBe(Date.parse("2026-09-16T04:40:57Z"));
		expect(gpt.status).toBe("ok");
	});

	it("reports account, planType, notes and limits from recorded response", () => {
		const state = createUsageState(antigravityOauthCredential, 1_789_514_000_000);
		const step = antigravityUsageFeed.advance(state, [
			{ status: 200, body: JSON.stringify(fixture("antigravity-quota.json")) },
		]);
		if (!("report" in step)) throw new Error("expected a report");
		expect(step.report.planType).toBe("Cloud Code");
		expect(step.report.accountId).toBe("aicode-consumers");
		expect(step.report.email).toBe("dev@example.com");
		expect(step.report.notes).toEqual(["Project: aicode-consumers"]);
		expect(step.report.error).toBeNull();
		expect(step.report.limits).toHaveLength(3);
	});

	it("reports unauthenticated error as data without throwing", async () => {
		const unauthBody = JSON.stringify({
			error: {
				code: 401,
				message: "Request had invalid authentication credentials.",
				status: "UNAUTHENTICATED",
			},
		});
		const report = await runUsageFeed(antigravityUsageFeed, antigravityOauthCredential, {
			fetchImpl: scriptedFetch([{ status: 401, body: unauthBody }]),
		});
		expect(report.error).toMatch(/HTTP 401: Request had invalid authentication credentials/);
		expect(report.limits).toEqual([]);
	});

	it("runs full host loop with runUsageFeed", async () => {
		const report = await runUsageFeed(antigravityUsageFeed, antigravityOauthCredential, {
			nowMs: 1_789_514_000_000,
			fetchImpl: scriptedFetch([{ status: 200, body: JSON.stringify(fixture("antigravity-quota.json")) }]),
		});
		expect(report.provider).toBe("google-antigravity");
		expect(report.providerName).toBe("Google Antigravity");
		expect(report.limits).toHaveLength(3);
		expect(report.error).toBeNull();
	});
});

describe("Charm Hyper usage feed", () => {
	it("accepts both oauth and apiKey credentials, rejecting session", () => {
		const stepOAuth = hyperUsageFeed.advance(createUsageState(hyperOauthCredential, 0), []);
		if (!("requests" in stepOAuth)) throw new Error("expected requests");
		expect(httpRequest(stepOAuth.requests[0]!).headers?.Authorization).toBe("Bearer hyper-oauth-access-token");

		const stepApiKey = hyperUsageFeed.advance(createUsageState(hyperApiKeyCredential, 0), []);
		if (!("requests" in stepApiKey)) throw new Error("expected requests");
		expect(httpRequest(stepApiKey.requests[0]!).headers?.Authorization).toBe("Bearer sk-hyper-test1234567890abcdef");

		const stepSession = hyperUsageFeed.advance(createUsageState({ kind: "session", session: {} }, 0), []);
		if (!("report" in stepSession)) throw new Error("expected a report");
		expect(stepSession.report.error).toMatch(/needs an OAuth or apiKey credential/);
	});

	it("executes the two-step flow: teams discovery then team billing + members", () => {
		const state = createUsageState(hyperOauthCredential, 1_789_514_000_000);

		// Round 0 -> asks for /v1/teams
		const step0 = hyperUsageFeed.advance(state, []);
		if (!("requests" in step0)) throw new Error("expected requests");
		expect(step0.requests).toHaveLength(1);
		expect(httpRequest(step0.requests[0]!).url).toBe("https://hyper.charm.land/v1/teams");

		// Round 1 -> receives teams, asks for billing and members
		const teamsResponse = { status: 200, body: JSON.stringify(fixture("hyper-teams.json")) };
		const step1 = hyperUsageFeed.advance(state, [teamsResponse]);
		if (!("requests" in step1)) throw new Error("expected requests");
		expect(step1.requests).toHaveLength(2);
		expect(httpRequest(step1.requests[0]!).url).toBe("https://hyper.charm.land/v1/teams/team_20439d8c54af/billing");
		expect(httpRequest(step1.requests[1]!).url).toBe("https://hyper.charm.land/v1/teams/team_20439d8c54af/members");

		// Round 2 -> receives billing and members, reports
		const billingResponse = { status: 200, body: JSON.stringify(fixture("hyper-billing.json")) };
		const membersResponse = { status: 200, body: JSON.stringify(fixture("hyper-members.json")) };
		const step2 = hyperUsageFeed.advance(state, [billingResponse, membersResponse]);
		if (!("report" in step2)) throw new Error("expected a report");

		expect(step2.report.planType).toBe("shred");
		expect(step2.report.accountId).toBe("team_20439d8c54af");
		expect(step2.report.email).toBe("user@example.com");
		expect(step2.report.balance).toBe("11.83");
		expect(step2.report.limits).toHaveLength(1);

		const limit = step2.report.limits[0]!;
		expect(limit.label).toBe("Primary");
		expect(limit.usedPercent).toBe(5.2);
		expect(limit.detail).toBe("237 of 250 credits");
		expect(limit.windowLabel).toBe("24h window");
		expect(limit.resetsAt).toBe(Date.parse("2026-09-16T22:26:09.702198Z"));
		expect(limit.status).toBe("ok");
	});

	it("converts unit fractions, timestamps, and balance correctly", () => {
		const raw = fixture("hyper-billing.json");
		const { planType, limits, balance } = parseHyperBilling(raw, 1_789_514_000_000);

		expect(planType).toBe("shred");
		expect(balance).toBe("11.83");
		expect(limits).toHaveLength(1);
		expect(limits[0]).toMatchObject({
			label: "Primary",
			usedPercent: 5.2,
			windowLabel: "24h window",
			status: "ok",
			detail: "237 of 250 credits",
		});
		expect(limits[0]!.resetsAt).toBe(1789597569702);
	});

	it("handles direct 1-step billing response for testing or single-step hosts", () => {
		const state = createUsageState(hyperApiKeyCredential, 1_789_514_000_000);
		const billingResponse = { status: 200, body: JSON.stringify(fixture("hyper-billing.json")) };
		const step = hyperUsageFeed.advance(state, [billingResponse]);
		if (!("report" in step)) throw new Error("expected a report");
		expect(step.report.accountId).toBe("team_20439d8c54af");
		expect(step.report.planType).toBe("shred");
		expect(step.report.balance).toBe("11.83");
		expect(step.report.limits[0]!.usedPercent).toBe(5.2);
	});

	it("reports authentication failure on teams endpoint as data without throwing", async () => {
		const report = await runUsageFeed(hyperUsageFeed, hyperOauthCredential, {
			fetchImpl: scriptedFetch([{ status: 401, body: JSON.stringify({ error: "authentication failed" }) }]),
		});
		expect(report.error).toMatch(/HTTP 401: authentication failed/);
		expect(report.limits).toEqual([]);
	});

	it("runs full two-round host loop with runUsageFeed", async () => {
		const report = await runUsageFeed(hyperUsageFeed, hyperOauthCredential, {
			nowMs: 1_789_514_000_000,
			fetchImpl: scriptedFetch([
				{ status: 200, body: JSON.stringify(fixture("hyper-teams.json")) },
				{ status: 200, body: JSON.stringify(fixture("hyper-billing.json")) },
				{ status: 200, body: JSON.stringify(fixture("hyper-members.json")) },
			]),
		});

		expect(report.provider).toBe("hyper");
		expect(report.providerName).toBe("Charm Hyper");
		expect(report.accountId).toBe("team_20439d8c54af");
		expect(report.email).toBe("user@example.com");
		expect(report.planType).toBe("shred");
		expect(report.balance).toBe("11.83");
		expect(report.limits).toHaveLength(1);
		expect(report.limits[0]!.usedPercent).toBe(5.2);
		expect(report.error).toBeNull();
	});
});
