/**
 * Capability Normalization Tests
 *
 * Tests the deprecation alias layer for plugin capability names. The runtime
 * never sees deprecated names — `normalizeCapability()` rewrites them at
 * every external boundary (definePlugin, adaptSandboxEntry, marketplace
 * diff). These tests pin the rename map and the normalization helpers so
 * that the alias layer keeps working until the deprecated names are
 * removed in the next minor.
 *
 * @see Issue: "Plugin capability names are inconsistent"
 */

import { describe, it, expect } from "vitest";

import {
	CAPABILITY_RENAMES,
	isDeprecatedCapability,
	normalizeCapabilities,
	normalizeCapability,
} from "../../../src/plugins/types.js";
import type { DeprecatedPluginCapability } from "../../../src/plugins/types.js";

describe("CAPABILITY_RENAMES", () => {
	it("maps every deprecated name to its current replacement", () => {
		// Pin the rename table — if the issue's table changes, this test
		// catches the drift. Anyone adding a deprecation should update
		// this case explicitly.
		expect(CAPABILITY_RENAMES).toEqual({
			"network:fetch": "network:request",
			"network:fetch:any": "network:request:unrestricted",
			"read:content": "content:read",
			"write:content": "content:write",
			"read:media": "media:read",
			"write:media": "media:write",
			"read:users": "users:read",
			"email:provide": "hooks.email-transport:register",
			"email:intercept": "hooks.email-events:register",
			"page:inject": "hooks.page-fragments:register",
		});
	});

	it("is frozen — cannot be mutated at runtime", () => {
		// `Object.freeze` makes the rename table tamper-proof.
		expect(Object.isFrozen(CAPABILITY_RENAMES)).toBe(true);
	});
});

describe("isDeprecatedCapability", () => {
	it("returns true for every deprecated name in the rename table", () => {
		for (const cap of Object.keys(CAPABILITY_RENAMES) as DeprecatedPluginCapability[]) {
			expect(isDeprecatedCapability(cap)).toBe(true);
		}
	});

	it("returns false for current capability names", () => {
		const current = [
			"content:read",
			"content:write",
			"media:read",
			"media:write",
			"users:read",
			"network:request",
			"network:request:unrestricted",
			"email:send",
			"hooks.email-transport:register",
			"hooks.email-events:register",
			"hooks.page-fragments:register",
		];
		for (const cap of current) {
			expect(isDeprecatedCapability(cap)).toBe(false);
		}
	});

	it("returns false for unknown strings", () => {
		expect(isDeprecatedCapability("not:a:capability")).toBe(false);
		expect(isDeprecatedCapability("")).toBe(false);
		expect(isDeprecatedCapability("content")).toBe(false);
	});

	it("does not match Object.prototype keys", () => {
		// Regression: an `in` check against CAPABILITY_RENAMES would
		// also match inherited properties. Using `Object.prototype.hasOwnProperty`
		// (or `Object.hasOwn`) keeps the check scoped to own properties.
		// Without the guard, `normalizeCapability("toString")` would return
		// the prototype function reference, breaking the contract that
		// unknown strings are returned as-is.
		expect(isDeprecatedCapability("toString")).toBe(false);
		expect(isDeprecatedCapability("constructor")).toBe(false);
		expect(isDeprecatedCapability("hasOwnProperty")).toBe(false);
		expect(isDeprecatedCapability("__proto__")).toBe(false);
		expect(isDeprecatedCapability("valueOf")).toBe(false);
	});
});

describe("normalizeCapability", () => {
	it("rewrites deprecated names to current names", () => {
		expect(normalizeCapability("read:content")).toBe("content:read");
		expect(normalizeCapability("write:content")).toBe("content:write");
		expect(normalizeCapability("read:media")).toBe("media:read");
		expect(normalizeCapability("write:media")).toBe("media:write");
		expect(normalizeCapability("read:users")).toBe("users:read");
		expect(normalizeCapability("network:fetch")).toBe("network:request");
		expect(normalizeCapability("network:fetch:any")).toBe("network:request:unrestricted");
		expect(normalizeCapability("email:provide")).toBe("hooks.email-transport:register");
		expect(normalizeCapability("email:intercept")).toBe("hooks.email-events:register");
		expect(normalizeCapability("page:inject")).toBe("hooks.page-fragments:register");
	});

	it("leaves current names unchanged", () => {
		expect(normalizeCapability("content:read")).toBe("content:read");
		expect(normalizeCapability("network:request")).toBe("network:request");
		expect(normalizeCapability("hooks.email-transport:register")).toBe(
			"hooks.email-transport:register",
		);
	});

	it("passes through unknown strings unchanged", () => {
		// Downstream validators throw on unknown capabilities; the
		// normalizer's job is purely to translate the alias map.
		expect(normalizeCapability("invalid:capability")).toBe("invalid:capability");
		expect(normalizeCapability("")).toBe("");
	});

	it("returns Object.prototype keys as-is (does not return prototype values)", () => {
		// Regression: with an `in` check, `normalizeCapability("toString")`
		// would have returned `Object.prototype.toString` (a function).
		// The own-property guard ensures we always return a string.
		expect(normalizeCapability("toString")).toBe("toString");
		expect(normalizeCapability("constructor")).toBe("constructor");
		expect(normalizeCapability("__proto__")).toBe("__proto__");
	});
});

describe("normalizeCapabilities", () => {
	it("rewrites every deprecated name in an array", () => {
		const input = ["read:content", "write:content", "network:fetch"];
		const result = normalizeCapabilities(input);

		expect(result).toEqual(["content:read", "content:write", "network:request"]);
	});

	it("preserves order of first occurrence", () => {
		const result = normalizeCapabilities(["network:request", "read:content", "write:media"]);

		expect(result).toEqual(["network:request", "content:read", "media:write"]);
	});

	it("deduplicates by canonical name when both old and new are present", () => {
		// A plugin migrating from old to new might transiently declare
		// both — the normalizer must not produce duplicates.
		const result = normalizeCapabilities(["read:content", "content:read"]);

		expect(result).toEqual(["content:read"]);
	});

	it("deduplicates two deprecated names that map to the same current name", () => {
		// Defensive: if someone declares the same alias twice, the result
		// must still contain it only once.
		const result = normalizeCapabilities(["read:content", "read:content"]);

		expect(result).toEqual(["content:read"]);
	});

	it("returns empty array for empty input", () => {
		expect(normalizeCapabilities([])).toEqual([]);
	});

	it("does not mutate the input array", () => {
		const input = ["read:content", "write:content"];
		const snapshot = [...input];
		normalizeCapabilities(input);

		expect(input).toEqual(snapshot);
	});

	it("is idempotent — normalizing twice gives the same result", () => {
		const input = ["read:content", "write:media", "page:inject"];
		const once = normalizeCapabilities(input);
		const twice = normalizeCapabilities(once);

		expect(twice).toEqual(once);
	});
});
