import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { buildChildAgentDoctrine } from "../src/core/prompts/rlm.js";
import {
	beginRlmChildResultTurn,
	bindRlmChildContractSession,
	completeRlmChildResultTurn,
	failRlmChildResultTurn,
	formatRlmResultContractInstructions,
	loadRlmChildContract,
	markRlmChildResultTurnNotified,
	normalizeRlmChildContract,
	prepareRlmChildContract,
	RLM_RESULT_FILE_NAME,
	readRlmChildResult,
	settleRlmChildResultTurn,
	submitRlmChildResult,
	validateRlmChildResult,
} from "../src/core/rlm-contract.js";

const roots: string[] = [];

function temporaryDirectory(): string {
	const path = mkdtempSync(join(tmpdir(), "rlm-contract-test-"));
	roots.push(path);
	return path;
}

const schema = {
	type: "object",
	properties: {
		status: { type: "string", maxLength: 32 },
		finding_ids: { type: "array", items: { type: "string" } },
		private_notes: { type: "string" },
	},
	required: ["status", "finding_ids"],
	additionalProperties: false,
};

const rawContract = {
	result: { schema, sink: { kind: "file" }, required: true },
	context: {
		terminal: { payload: false, notify: true },
		direct_messages: { payload: false, notify: false },
		observe: false,
		result: { fields: ["status", "finding_ids"] },
	},
};

afterEach(() => {
	for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});

describe("RLM child contracts", () => {
	it("preserves native behavior when no contract fields are supplied", () => {
		expect(normalizeRlmChildContract(undefined, undefined)).toBeUndefined();
	});

	it("normalizes independent result and context policies", () => {
		const contract = normalizeRlmChildContract(rawContract.result, rawContract.context);
		expect(contract).toEqual(rawContract);
		expect(contract?.result?.schema).not.toBe(schema);
	});

	it("rejects ambiguous or unsupported policies", () => {
		expect(() => normalizeRlmChildContract(undefined, { result: { fields: [] } })).toThrow(
			"requires a result contract",
		);
		expect(() => normalizeRlmChildContract(rawContract.result, { result: { fields: ["missing"] } })).toThrow(
			"admits unknown schema field",
		);
		expect(() =>
			normalizeRlmChildContract(rawContract.result, { terminal: { payload: true, notify: false } }),
		).toThrow("cannot be false");
		expect(() => normalizeRlmChildContract({ ...rawContract.result, mode: "artifact" }, undefined)).toThrow(
			"unsupported fields",
		);
		expect(() =>
			normalizeRlmChildContract(
				{ schema: { type: "object", properties: { status: { type: "not-real" } } } },
				undefined,
			),
		).toThrow("type is unsupported");
		expect(() =>
			normalizeRlmChildContract(
				{ schema: { type: "object", properties: { status: { $ref: "https://example.com/schema" } } } },
				undefined,
			),
		).toThrow("unsupported fields");
		expect(() =>
			normalizeRlmChildContract(
				{ schema: { type: "object", properties: { count: { type: "number", multipleOf: 0 } } } },
				undefined,
			),
		).toThrow("greater than zero");
		expect(() =>
			normalizeRlmChildContract(
				{ schema: { type: "object", properties: { count: { const: Number.MAX_SAFE_INTEGER + 1 } } } },
				undefined,
			),
		).toThrow("safe range");
	});

	it("persists and binds a host-owned contract", () => {
		const root = temporaryDirectory();
		const contract = normalizeRlmChildContract(rawContract.result, rawContract.context)!;
		const prepared = prepareRlmChildContract(root, "sub-1234", "worker", contract)!;
		expect(prepared.resultPath).toBe(join(root, RLM_RESULT_FILE_NAME));
		expect(loadRlmChildContract(root)).toMatchObject({ childId: "sub-1234", sessionName: "worker" });

		const bound = bindRlmChildContractSession(prepared, "session-1234");
		expect(loadRlmChildContract(root)).toMatchObject({ sessionId: "session-1234" });
		const persisted = JSON.parse(readFileSync(bound.contractPath, "utf8"));
		expect(persisted.format_version).toBe(1);
		writeFileSync(bound.contractPath, JSON.stringify({ ...persisted, unexpected: true }));
		expect(() => loadRlmChildContract(root)).toThrow("unsupported fields");
	});

	it("validates turn-scoped results and projects only admitted fields", () => {
		const root = temporaryDirectory();
		const contract = normalizeRlmChildContract(rawContract.result, rawContract.context)!;
		let prepared = prepareRlmChildContract(root, "sub-1234", "worker", contract)!;
		const initial = beginRlmChildResultTurn(prepared, "action-0");
		prepared = initial.prepared;
		const value = { status: "approved", finding_ids: ["F1"], private_notes: "do not admit" };
		expect(() =>
			submitRlmChildResult(prepared, {
				...value,
				finding_ids: [Number.MAX_SAFE_INTEGER + 1],
			}),
		).toThrow("safe range");
		const submission = submitRlmChildResult(prepared, value);
		expect(submission).toMatchObject({ generation: 0, path: join(root, RLM_RESULT_FILE_NAME) });
		expect(submission.sha256).toMatch(/^[0-9a-f]{64}$/);
		expect(submitRlmChildResult(prepared, value)).toEqual(submission);
		expect(() => submitRlmChildResult(prepared, { ...value, finding_ids: [] })).toThrow(
			"already submitted with different content",
		);
		prepared = settleRlmChildResultTurn(prepared, "action-0").prepared;
		const full = readRlmChildResult(prepared);
		expect(full).toMatchObject({ generation: 0, result: { status: "approved", finding_ids: ["F1"] } });
		expect(full.sha256).toMatch(/^[0-9a-f]{64}$/);
		expect(readRlmChildResult(prepared, ["status"]).result).toEqual({ status: "approved" });
		expect(() => readRlmChildResult(prepared, ["private_notes"])).toThrow("exceed the child context contract");
	});

	it("omits absent optional fields from direct and wire projections", () => {
		const root = temporaryDirectory();
		const contract = normalizeRlmChildContract(rawContract.result, {
			...rawContract.context,
			result: { fields: ["private_notes"] },
		})!;
		let prepared = prepareRlmChildContract(root, "sub-optional-field", "worker", contract)!;
		prepared = beginRlmChildResultTurn(prepared, "action-0").prepared;
		submitRlmChildResult(prepared, { status: "approved", finding_ids: [] });
		prepared = settleRlmChildResultTurn(prepared, "action-0").prepared;
		expect(readRlmChildResult(prepared).result).toEqual({});
		expect(JSON.parse(JSON.stringify(readRlmChildResult(prepared).result))).toEqual({});
	});

	it("retains old settled generations while a follow-up result is active", () => {
		const root = temporaryDirectory();
		const contract = normalizeRlmChildContract(rawContract.result, rawContract.context)!;
		let prepared = prepareRlmChildContract(root, "sub-1234", "worker", contract)!;
		prepared = beginRlmChildResultTurn(prepared, "action-0").prepared;
		submitRlmChildResult(prepared, { status: "initial", finding_ids: [] });
		prepared = settleRlmChildResultTurn(prepared, "action-0").prepared;

		const followup = beginRlmChildResultTurn(prepared, "action-1");
		prepared = followup.prepared;
		expect(followup.turn).toMatchObject({ generation: 1, status: "active" });
		expect(readRlmChildResult(prepared).result).toEqual({ status: "initial", finding_ids: [] });
		submitRlmChildResult(prepared, { status: "follow-up", finding_ids: ["F2"] });
		prepared = settleRlmChildResultTurn(prepared, "action-1").prepared;
		expect(readRlmChildResult(prepared)).toMatchObject({
			generation: 1,
			result: { status: "follow-up", finding_ids: ["F2"] },
		});
		expect(readRlmChildResult(prepared, undefined, 0).result).toEqual({ status: "initial", finding_ids: [] });
		expect(beginRlmChildResultTurn(loadRlmChildContract(root)!, "action-1").turn).toMatchObject({
			generation: 1,
			status: "settled",
		});
	});

	it("persists lifecycle-only turns without inventing notice admission", () => {
		const root = temporaryDirectory();
		let prepared = prepareRlmChildContract(root, "sub-plain", "plain-worker", {})!;
		prepared = bindRlmChildContractSession(prepared, "session-plain");
		prepared = beginRlmChildResultTurn(prepared, "parent-turn").prepared;
		prepared = completeRlmChildResultTurn(prepared, "parent-turn", false).prepared;
		prepared = settleRlmChildResultTurn(prepared, "parent-turn").prepared;
		expect(loadRlmChildContract(root)?.resultState?.turn).toMatchObject({
			status: "settled",
			provider_completed: true,
			replied_to_parent: false,
			notice_admitted: false,
		});

		const persisted = JSON.parse(readFileSync(prepared.contractPath, "utf8"));
		delete persisted.result_state.turn.notice_admitted;
		writeFileSync(prepared.contractPath, `${JSON.stringify(persisted)}\n`);
		prepared = loadRlmChildContract(root)!;
		expect(prepared.resultState?.turn?.notice_admitted).toBe(false);
		prepared = markRlmChildResultTurnNotified(prepared, "parent-turn");
		expect(loadRlmChildContract(root)?.resultState?.turn?.notice_admitted).toBe(true);
	});

	it("persists provider completion and notice admission for replay", () => {
		const root = temporaryDirectory();
		const contract = normalizeRlmChildContract(rawContract.result, rawContract.context)!;
		let prepared = prepareRlmChildContract(root, "sub-1234", "worker", contract)!;
		prepared = beginRlmChildResultTurn(prepared, "action-0").prepared;
		submitRlmChildResult(prepared, { status: "approved", finding_ids: [] });
		prepared = completeRlmChildResultTurn(prepared, "action-0", true).prepared;
		expect(loadRlmChildContract(root)?.resultState?.turn).toMatchObject({
			provider_completed: true,
			replied_to_parent: true,
			notice_admitted: false,
		});
		prepared = settleRlmChildResultTurn(prepared, "action-0").prepared;
		expect(loadRlmChildContract(root)?.resultState?.turn).toMatchObject({
			status: "settled",
			notice_admitted: false,
		});
		prepared = markRlmChildResultTurnNotified(prepared, "action-0");
		expect(loadRlmChildContract(root)?.resultState?.turn).toMatchObject({
			status: "settled",
			notice_admitted: true,
		});

		const failedRoot = temporaryDirectory();
		let failed = beginRlmChildResultTurn(
			prepareRlmChildContract(failedRoot, "sub-failed", "worker", contract)!,
			"action-failed",
		).prepared;
		failed = failRlmChildResultTurn(failed, "action-failed", "provider failed");
		expect(loadRlmChildContract(failedRoot)?.resultState?.turn).toMatchObject({
			status: "failed",
			notice_admitted: false,
			error: "provider failed",
		});
	});

	it("fails closed for invalid and symlinked active-turn results", () => {
		const root = temporaryDirectory();
		const contract = normalizeRlmChildContract(rawContract.result, rawContract.context)!;
		let prepared = beginRlmChildResultTurn(
			prepareRlmChildContract(root, "sub-1234", "worker", contract)!,
			"action-invalid",
		).prepared;
		expect(() => submitRlmChildResult(prepared, { status: 3, finding_ids: [] })).toThrow("does not match its schema");
		writeFileSync(prepared.resultPath!, JSON.stringify({ status: 3, finding_ids: [] }));
		const invalid = settleRlmChildResultTurn(prepared, "action-invalid");
		expect(invalid.error).toContain("does not match its schema");
		expect(() => validateRlmChildResult(invalid.prepared)).toThrow("no settled result");

		const symlinkRoot = temporaryDirectory();
		prepared = beginRlmChildResultTurn(
			prepareRlmChildContract(symlinkRoot, "sub-5678", "worker-2", contract)!,
			"action-symlink",
		).prepared;
		const target = join(symlinkRoot, "outside.json");
		writeFileSync(target, JSON.stringify({ status: "approved", finding_ids: [] }));
		symlinkSync(target, prepared.resultPath!);
		expect(settleRlmChildResultTurn(prepared, "action-symlink").error).toContain("not a regular file");
	});

	it("allows optional result turns to settle without claiming a ready generation", () => {
		const root = temporaryDirectory();
		const contract = normalizeRlmChildContract({ ...rawContract.result, required: false }, rawContract.context)!;
		let prepared = prepareRlmChildContract(root, "sub-optional", "optional-worker", contract)!;
		prepared = beginRlmChildResultTurn(prepared, "action-optional").prepared;
		prepared = completeRlmChildResultTurn(prepared, "action-optional", false).prepared;
		prepared = settleRlmChildResultTurn(prepared, "action-optional").prepared;
		expect(prepared.resultState).toMatchObject({ turn: { status: "settled" } });
		expect(prepared.resultState?.latest_settled).toBeUndefined();
		expect(formatRlmResultContractInstructions(prepared)).toContain("submission is optional");
		expect(
			buildChildAgentDoctrine({
				depth: 1,
				activeTools: ["ipython"],
				resultContract: true,
				resultRequired: false,
			}),
		).toContain("omission is allowed");
	});

	it("rejects unsafe integers written directly to result artifacts", () => {
		const unsafeRoot = temporaryDirectory();
		const integerContract = normalizeRlmChildContract(
			{
				schema: {
					type: "object",
					properties: { count: { type: "integer" } },
					required: ["count"],
					additionalProperties: false,
				},
			},
			{ result: { fields: ["count"] } },
		)!;
		const unsafe = beginRlmChildResultTurn(
			prepareRlmChildContract(unsafeRoot, "sub-unsafe", "worker", integerContract)!,
			"action-unsafe",
		).prepared;
		writeFileSync(unsafe.resultPath!, '{"count":9007199254740993}\n');
		const rejected = settleRlmChildResultTurn(unsafe, "action-unsafe");
		expect(rejected.error).toContain("safe range");

		const safeRoot = temporaryDirectory();
		let safe = beginRlmChildResultTurn(
			prepareRlmChildContract(safeRoot, "sub-safe", "worker", integerContract)!,
			"action-safe",
		).prepared;
		writeFileSync(safe.resultPath!, `{"count":${Number.MAX_SAFE_INTEGER}}\n`);
		safe = settleRlmChildResultTurn(safe, "action-safe").prepared;
		expect(readRlmChildResult(safe).result).toEqual({ count: Number.MAX_SAFE_INTEGER });
	});

	it("makes structured submission authoritative in child doctrine", () => {
		const doctrine = buildChildAgentDoctrine({
			depth: 1,
			activeTools: ["ipython"],
			resultContract: true,
			directMessagePayload: false,
		});
		expect(doctrine).toContain("await rlm.submit_result(value)");
		expect(doctrine).not.toContain("await rlm.send_parent");
		expect(doctrine).not.toContain("reply explicitly");
	});

	it("renders the file contract without inventing a named mode", () => {
		const root = temporaryDirectory();
		const contract = normalizeRlmChildContract(rawContract.result, rawContract.context)!;
		const prepared = prepareRlmChildContract(root, "sub-1234", "worker", contract)!;
		const instructions = formatRlmResultContractInstructions(prepared);
		expect(instructions).toContain("await rlm.submit_result(value)");
		expect(instructions).toContain('"finding_ids"');
		expect(instructions).not.toContain("artifact-only");
	});

	describe("guarded-by-default context", () => {
		it("withholds terminal, direct messages, and observation when a result is declared without context", () => {
			const contract = normalizeRlmChildContract(
				{ schema: { type: "object", properties: {} }, required: false },
				undefined,
				{ guardedByDefault: true },
			)!;
			expect(contract.context).toEqual({
				terminal: { payload: false, notify: true },
				direct_messages: { payload: false, notify: true },
				observe: false,
			});
		});

		it("keeps explicit context entries and fills the rest with guarded defaults", () => {
			const contract = normalizeRlmChildContract(
				{ schema: { type: "object", properties: {} }, required: false },
				{ direct_messages: { payload: true } },
				{ guardedByDefault: true },
			)!;
			expect(contract.context).toEqual({
				terminal: { payload: false, notify: true },
				direct_messages: { payload: true, notify: true },
				observe: false,
			});
		});

		it("does not add guarded defaults when the caller opted out", () => {
			expect(normalizeRlmChildContract(undefined, undefined, { guardedByDefault: true })).toBeUndefined();
			expect(
				normalizeRlmChildContract({ schema: { type: "object", properties: {} }, required: false }, undefined, {
					guardedByDefault: false,
				})?.context,
			).toBeUndefined();
		});

		it("renders guarded doctrine for a child whose payloads are withheld", () => {
			const doctrine = buildChildAgentDoctrine({
				depth: 1,
				activeTools: ["ipython"],
				resultContract: true,
				resultRequired: true,
				contextGuarded: true,
			});
			expect(doctrine).toContain("your only communication channel");
			expect(doctrine).not.toContain("only for coordination or clarification");
		});
	});
});
