import assert from "node:assert/strict";
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";

import { clearOutputMetrics, getOutputMetricsSummary, trackOutputSavings } from "./output-metrics.ts";
import { mock, runTest } from "./test-helpers.test.ts";
import { matchesCommandPatterns, normalizeCommandForDetection } from "./techniques/command-detection.ts";
import { compactPath } from "./techniques/path-utils.ts";
import { filterAggressive } from "./techniques/source.ts";
import { aggregateTestOutput } from "./techniques/test-output.ts";
import { applyWindowsBashCompatibilityFixes } from "./windows-command-helpers.ts";
import { applyRewrittenCommandShellSafetyFixups } from "./rewrite-pipeline-safety.ts";
import { applyRtkCommandEnvironment } from "./rtk-command-environment.ts";
import { sanitizeStreamingBashExecutionResult } from "./tool-execution-sanitizer.ts";

mock.module("@earendil-works/pi-coding-agent", {
	namedExports: {
		getAgentDir: () => "/tmp/.pi/agent",
	},
});

const {
	ensureConfigExists,
	getRtkIntegrationConfigPath,
	loadRtkIntegrationConfig,
	normalizeRtkIntegrationConfig,
	saveRtkIntegrationConfig,
} = await import("./config-store.ts");

function makeTempConfigPath(): string {
	return `${getRtkIntegrationConfigPath()}.test-${Date.now()}-${Math.random().toString(16).slice(2)}.json`;
}

function cleanupFile(path: string): void {
	for (const candidate of [path, `${path}.tmp`]) {
		try {
			if (existsSync(candidate)) {
				unlinkSync(candidate);
			}
		} catch {
			// Ignore cleanup failures in tests.
		}
	}
}

runTest("config-store normalizes invalid values and clamps numeric ranges", () => {
	const normalized = normalizeRtkIntegrationConfig({
		enabled: "yes",
		mode: "invalid",
		rewriteGitGithub: false,
		outputCompaction: {
			stripAnsi: false,
			sourceCodeFilteringEnabled: "sometimes",
			sourceCodeFiltering: "extreme",
			truncate: {
				enabled: true,
				maxChars: 12,
			},
			smartTruncate: {
				enabled: true,
				maxLines: 999_999,
			},
			trackSavings: false,
		},
	});

	assert.equal(normalized.enabled, true);
	assert.equal(normalized.mode, "rewrite");
	assert.equal(Object.hasOwn(normalized, "rewriteGitGithub"), false);
	assert.equal(normalized.outputCompaction.stripAnsi, false);
	assert.equal(normalized.outputCompaction.readCompaction.enabled, true);
	assert.equal(normalized.outputCompaction.sourceCodeFilteringEnabled, true);
	assert.equal(normalized.outputCompaction.sourceCodeFiltering, "minimal");
	assert.equal(normalized.outputCompaction.truncate.maxChars, 1_000);
	assert.equal(normalized.outputCompaction.smartTruncate.maxLines, 4_000);
	assert.equal(normalized.outputCompaction.trackSavings, false);
});

runTest("config-store uses safer read defaults when readCompaction is explicit", () => {
	const normalized = normalizeRtkIntegrationConfig({
		outputCompaction: {
			readCompaction: { enabled: false },
		},
	});

	assert.equal(normalized.outputCompaction.readCompaction.enabled, false);
	assert.equal(normalized.outputCompaction.sourceCodeFilteringEnabled, false);
	assert.equal(normalized.outputCompaction.sourceCodeFiltering, "none");
	assert.equal(normalized.outputCompaction.smartTruncate.enabled, false);
});

runTest("config-store can ensure, save, and reload isolated config files", () => {
	const tempPath = makeTempConfigPath();
	cleanupFile(tempPath);

	try {
		const ensured = ensureConfigExists(tempPath);
		assert.equal(ensured.error, undefined);
		assert.equal(existsSync(tempPath), true);

		const defaultLoad = loadRtkIntegrationConfig(tempPath);
		assert.equal(defaultLoad.warning, undefined);
		assert.equal(defaultLoad.config.mode, "rewrite");
		assert.equal(defaultLoad.config.outputCompaction.readCompaction.enabled, false);

		const saved = saveRtkIntegrationConfig(
			{
				...defaultLoad.config,
				mode: "suggest",
				outputCompaction: {
					...defaultLoad.config.outputCompaction,
					truncate: {
						...defaultLoad.config.outputCompaction.truncate,
						maxChars: 250_000,
					},
				},
			},
			tempPath,
		);
		assert.equal(saved.success, true);

		const reloaded = loadRtkIntegrationConfig(tempPath);
		assert.equal(reloaded.config.mode, "suggest");
		assert.equal(reloaded.config.outputCompaction.truncate.maxChars, 200_000);
		assert.ok(readFileSync(tempPath, "utf-8").endsWith("\n"));
	} finally {
		cleanupFile(tempPath);
	}
});

runTest("config-store falls back to defaults when JSON is invalid", () => {
	const tempPath = makeTempConfigPath();
	cleanupFile(tempPath);

	try {
		writeFileSync(tempPath, "{not valid json", "utf-8");
		const loaded = loadRtkIntegrationConfig(tempPath);
		assert.equal(loaded.config.mode, "rewrite");
		assert.ok((loaded.warning ?? "").includes(tempPath));
		assert.ok((loaded.warning ?? "").includes("Failed to parse"));
	} finally {
		cleanupFile(tempPath);
	}
});

runTest("config-store malformed-file defaults are isolated from caller mutation", () => {
	const tempPath = makeTempConfigPath();
	cleanupFile(tempPath);

	try {
		writeFileSync(tempPath, "{not valid json", "utf-8");
		const firstLoad = loadRtkIntegrationConfig(tempPath);
		firstLoad.config.outputCompaction.truncate.maxChars = 42_424;
		firstLoad.config.outputCompaction.readCompaction.enabled = true;

		const secondLoad = loadRtkIntegrationConfig(tempPath);

		assert.equal(secondLoad.config.outputCompaction.truncate.maxChars, 12_000);
		assert.equal(secondLoad.config.outputCompaction.readCompaction.enabled, false);
	} finally {
		cleanupFile(tempPath);
	}
});

runTest("output metrics summarize tracked savings and clear state", () => {
	clearOutputMetrics();
	assert.equal(getOutputMetricsSummary(), "RTK output compaction metrics: no data yet.");

	const first = trackOutputSavings("1234567890", "12345", "bash", ["ansi", "truncate"]);
	assert.equal(first.tool, "bash");
	assert.equal(first.techniques, "ansi,truncate");
	assert.equal(first.savingsPercent, 50);

	trackOutputSavings("123456", "1234", "read", []);
	const summary = getOutputMetricsSummary();
	assert.ok(summary.includes("calls=2, saved=7 chars (43.8%)"));
	assert.ok(summary.includes("- bash: 1 calls, saved 5 chars (50.0%)"));
	assert.ok(summary.includes("- read: 1 calls, saved 2 chars (33.3%)"));

	clearOutputMetrics();
	assert.equal(getOutputMetricsSummary(), "RTK output compaction metrics: no data yet.");
});

runTest("aggressive source filtering ignores string and inline comment braces while tracking implementation blocks", () => {
	const withLiteralBrace = [
		"function first() {",
		'  const value = "{";',
		"  return value;",
		"}",
		"function second() {",
		"  return true;",
		"}",
	].join("\n");
	const withoutLiteralBrace = [
		"function first() {",
		'  const value = "plain";',
		"  return value;",
		"}",
		"function second() {",
		"  return true;",
		"}",
	].join("\n");
	const withInlineCommentBrace = [
		"function first() {",
		"  const value = 1; // {",
		"  return value;",
		"}",
		"function second() {",
		"  return true;",
		"}",
	].join("\n");
	const withoutInlineCommentBrace = [
		"function first() {",
		"  const value = 1; // no brace",
		"  return value;",
		"}",
		"function second() {",
		"  return true;",
		"}",
	].join("\n");

	assert.equal(filterAggressive(withLiteralBrace, "typescript"), filterAggressive(withoutLiteralBrace, "typescript"));
	assert.equal(filterAggressive(withInlineCommentBrace, "typescript"), filterAggressive(withoutInlineCommentBrace, "typescript"));
});

runTest("test output fallback counts unicode pass and fail symbols", () => {
	const result = aggregateTestOutput("✓ creates user\n✔ updates user\n✕ deletes user\n✗ archives user\n", "bun test");

	assert.ok(result?.includes("PASS: 2 passed"));
	assert.ok(result?.includes("FAIL: 2 failed"));
});

runTest("command detection ignores env prefixes, blank lines, and chained suffixes", () => {
	assert.equal(normalizeCommandForDetection("NODE_ENV=test FOO=bar npm test && echo done"), "npm test");
	assert.equal(normalizeCommandForDetection("\n\n PYTHONPATH=src git status\n echo later"), "git status");
	assert.equal(normalizeCommandForDetection("   "), null);
	assert.equal(matchesCommandPatterns("CI=1 bun test | head -5", [/^bun test/]), true);
	assert.equal(matchesCommandPatterns("echo hello", [/^bun test/]), false);
});

runTest("RTK command environment preserves explicit leading RTK_DB_PATH overrides", () => {
	const command = 'RTK_DB_PATH="/custom/history.db" rtk git diff';
	assert.equal(applyRtkCommandEnvironment(command), command);

	const singleQuotedCommand = "RTK_DB_PATH='/custom/it'\\''s/history.db' rtk git diff";
	assert.equal(applyRtkCommandEnvironment(singleQuotedCommand), singleQuotedCommand);

	const exportedCommand = 'export RTK_DB_PATH="/custom/history.db"; rtk git diff';
	assert.equal(applyRtkCommandEnvironment(exportedCommand), exportedCommand);
});

runTest("RTK command environment respects inherited RTK_DB_PATH values", () => {
	const previousRtkDbPath = process.env.RTK_DB_PATH;
	const command = "rtk git status";

	try {
		process.env.RTK_DB_PATH = "/persistent/shared/history.db";

		assert.equal(applyRtkCommandEnvironment(command), command);
	} finally {
		if (previousRtkDbPath === undefined) {
			delete process.env.RTK_DB_PATH;
		} else {
			process.env.RTK_DB_PATH = previousRtkDbPath;
		}
	}
});

runTest("RTK command environment ignores blank inherited RTK_DB_PATH values", () => {
	const previousRtkDbPath = process.env.RTK_DB_PATH;

	try {
		process.env.RTK_DB_PATH = "   ";

		assert.match(applyRtkCommandEnvironment("rtk git status"), /^export RTK_DB_PATH=/);
	} finally {
		if (previousRtkDbPath === undefined) {
			delete process.env.RTK_DB_PATH;
		} else {
			process.env.RTK_DB_PATH = previousRtkDbPath;
		}
	}
});

runTest("RTK command environment single-quotes hostile temp paths", () => {
	const previousTmpDir = process.env.TMPDIR;
	const previousTmp = process.env.TMP;
	const previousTemp = process.env.TEMP;
	const hostilePath = process.platform === "win32" ? "C:\\Temp\\$(touch owned)`bad`'dir" : "/tmp/$(touch owned)`bad`'dir";

	try {
		process.env.TMPDIR = hostilePath;
		process.env.TMP = hostilePath;
		process.env.TEMP = hostilePath;

		const rewritten = applyRtkCommandEnvironment("rtk git status");
		assert.ok(rewritten.startsWith("export RTK_DB_PATH='"));
		assert.ok(rewritten.includes("$(touch owned)`bad`'\\''dir"));
		assert.ok(rewritten.endsWith("; rtk git status"));
		assert.equal(/^export RTK_DB_PATH=\"/.test(rewritten), false);
	} finally {
		process.env.TMPDIR = previousTmpDir;
		process.env.TMP = previousTmp;
		process.env.TEMP = previousTemp;
	}
});

runTest("path compaction preserves the tail and handles Windows separators", () => {
	const unixPath = "/Users/example/projects/pi-rtk-optimizer/src/techniques/path-utils.ts";
	const compactUnixPath = compactPath(unixPath, 28);
	assert.ok(compactUnixPath.length <= 28);
	assert.ok(compactUnixPath.endsWith("path-utils.ts"));
	assert.ok(compactUnixPath.includes("/"));

	const windowsPath = "C:\\Users\\Administrator\\Documents\\pi-rtk-optimizer\\src\\windows-command-helpers.ts";
	const compactWindowsPath = compactPath(windowsPath, 30);
	assert.ok(compactWindowsPath.length <= 30);
	assert.equal(compactWindowsPath.includes("\\"), true);
	assert.ok(compactWindowsPath.endsWith("windows-command-helpers.ts"));

	assert.equal(compactPath("src/file.ts", 40), "src/file.ts");
});

runTest("windows bash compatibility rewrites only when the runtime is Windows", () => {
	const command = "cd /d C:\\Users\\Administrator\\project && python script.py";
	const fixed = applyWindowsBashCompatibilityFixes(command, "win32");
	assert.deepEqual(fixed.applied, ["cd-/d", "python-utf8"]);
	assert.equal(
		fixed.command,
		'PYTHONIOENCODING=utf-8 cd "C:/Users/Administrator/project" && python script.py',
	);

	const unchanged = applyWindowsBashCompatibilityFixes(command, "linux");
	assert.deepEqual(unchanged.applied, []);
	assert.equal(unchanged.command, command);

	const alreadyUtf8 = applyWindowsBashCompatibilityFixes("PYTHONIOENCODING=utf-8 python script.py", "win32");
	assert.deepEqual(alreadyUtf8.applied, []);
	assert.equal(alreadyUtf8.command, "PYTHONIOENCODING=utf-8 python script.py");
});

runTest("windows bash compatibility rewrites compound cd slash-d operators", () => {
	assert.equal(
		applyWindowsBashCompatibilityFixes("cd /d C:\\work || echo failed", "win32").command,
		'cd "C:/work" || echo failed',
	);
	assert.equal(
		applyWindowsBashCompatibilityFixes("cd /d C:\\work ; echo done", "win32").command,
		'cd "C:/work" ; echo done',
	);
	assert.equal(
		applyWindowsBashCompatibilityFixes("cd /d C:\\work | cat", "win32").command,
		'cd "C:/work" | cat',
	);
	assert.equal(
		applyWindowsBashCompatibilityFixes('cd /d "C:\\work space" || echo failed', "win32").command,
		'cd "C:/work space" || echo failed',
	);
});

runTest("rewrite pipeline safety buffers rewritten Windows producer commands", () => {
	const rewritten = applyRewrittenCommandShellSafetyFixups("rtk git diff | grep TODO", "win32");
	assert.ok(rewritten.includes('mktemp'));
	assert.ok(rewritten.includes('trap'));
	assert.ok(rewritten.includes('rtk git diff > "$__pi_rtk_pipe_tmp"'));
	assert.ok(rewritten.includes('(grep TODO) < "$__pi_rtk_pipe_tmp"'));

	assert.equal(
		applyRewrittenCommandShellSafetyFixups("rtk git diff | grep TODO", "linux"),
		"rtk git diff | grep TODO",
	);
	assert.equal(applyRewrittenCommandShellSafetyFixups("git diff | grep TODO", "win32"), "git diff | grep TODO");
});

runTest("rewrite pipeline safety buffers leading pipelines before compound suffixes", () => {
	const andCommand = applyRewrittenCommandShellSafetyFixups("rtk git diff | grep TODO && echo done", "win32");
	assert.ok(andCommand.includes('(grep TODO) < "$__pi_rtk_pipe_tmp"'));
	assert.ok(andCommand.endsWith("&& echo done"));

	const orCommand = applyRewrittenCommandShellSafetyFixups("rtk git diff | grep TODO || echo none", "win32");
	assert.ok(orCommand.includes('(grep TODO) < "$__pi_rtk_pipe_tmp"'));
	assert.ok(orCommand.endsWith("|| echo none"));

	const semicolonCommand = applyRewrittenCommandShellSafetyFixups("rtk git diff | grep TODO; echo done", "win32");
	assert.ok(semicolonCommand.includes('(grep TODO) < "$__pi_rtk_pipe_tmp"'));
	assert.ok(semicolonCommand.endsWith("; echo done"));
});

runTest("rewrite pipeline safety keeps exported RTK_DB_PATH on rewritten producer commands", () => {
	const envScopedCommand = applyRtkCommandEnvironment("rtk git diff agent/extensions/pi-multi-auth/account-manager.ts | head -200");
	const rewritten = applyRewrittenCommandShellSafetyFixups(envScopedCommand, "win32");

	assert.ok(rewritten.startsWith("export RTK_DB_PATH="));
	assert.equal(rewritten.startsWith("RTK_DB_PATH="), false);
	assert.ok(rewritten.includes("; {"));
	assert.ok(
		rewritten.includes('rtk git diff agent/extensions/pi-multi-auth/account-manager.ts > "$__pi_rtk_pipe_tmp"'),
	);
	assert.ok(rewritten.includes('(head -200) < "$__pi_rtk_pipe_tmp"'));

	assert.equal(applyRewrittenCommandShellSafetyFixups(envScopedCommand, "linux"), envScopedCommand);
});

runTest("rewrite pipeline safety buffers explicit RTK_DB_PATH export preludes", () => {
	const command = 'export RTK_DB_PATH="/custom/history.db"; rtk git diff | head -200';
	const rewritten = applyRewrittenCommandShellSafetyFixups(command, "win32");

	assert.ok(rewritten.startsWith('export RTK_DB_PATH="/custom/history.db"; {'));
	assert.ok(rewritten.includes('rtk git diff > "$__pi_rtk_pipe_tmp"'));
	assert.ok(rewritten.includes('(head -200) < "$__pi_rtk_pipe_tmp"'));

	assert.equal(applyRewrittenCommandShellSafetyFixups(command, "linux"), command);
});

runTest("RTK command environment uses export prelude for shell compound commands", () => {
	const rewritten = applyRtkCommandEnvironment('for d in a b; do echo "$d"; done');
	assert.ok(/^export RTK_DB_PATH=/.test(rewritten));
	assert.ok(/; for d in a b; do echo "\$d"; done$/.test(rewritten));
});

runTest("streaming sanitizer strips ANSI codes and preserves non-text blocks", () => {
	const ansiResult = {
		content: [
			{ type: "text", text: "\x1B[32mworking tree clean\x1B[0m\n" },
			{ type: "image", url: "ignored" },
		],
	};
	const ansiSanitization = sanitizeStreamingBashExecutionResult(ansiResult, "rtk git status");
	assert.equal(ansiSanitization.changed, true);
	assert.equal(
		((ansiSanitization.result as typeof ansiResult).content[0] as { text: string }).text,
		"working tree clean\n",
	);
	assert.equal((ansiResult.content[0] as { text: string }).text, "\x1B[32mworking tree clean\x1B[0m\n");
	assert.deepEqual((ansiSanitization.result as typeof ansiResult).content[1], { type: "image", url: "ignored" });

	const plainResult = {
		content: [
			{
				type: "text",
				text: "[rtk] warning: builtin filters: parse failure\n\nworking tree clean\n",
			},
		],
	};
	const plainSanitization = sanitizeStreamingBashExecutionResult(plainResult, "rtk git status");
	assert.equal(plainSanitization.changed, false);
	assert.equal(plainSanitization.result, plainResult);
	assert.equal(
		(plainResult.content[0] as { text: string }).text,
		"[rtk] warning: builtin filters: parse failure\n\nworking tree clean\n",
	);
});

console.log("All additional coverage tests passed.");
