/**
 * tui-walkthrough-driver.ts — 공유 TUI PTY 드라이버
 *
 * 모든 walkthrough 테스트가 사용하는 공통 유틸리티.
 * bun-pty 기반 TUI 제어 + 에러 감지.
 */

import { spawn as PtySpawn, type IPty } from "bun-pty"
import path from "path"

const BIN = path.resolve(import.meta.dir, "../../bin/saeeol.cjs")

// ── ANSI 제거 ──

export function stripAnsi(s: string): string {
	return s
		.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
		.replace(/\x1b\].*?\x07/g, "")
		.replace(/\x1b\[.*?m/g, "")
		.replace(/[\x00-\x09\x0b\x0c\x0e-\x1f]/g, "")
}

// ── Known issues — 해결되면 배열에서 제거 ──

const KNOWN_ISSUES = [
	/export named 'jsx' not found in module.*@opentui\/solid\/jsx-runtime/i,
	/e=Export named 'jsx' not found.*exception/i,
]

export function hasError(plain: string): string[] {
	return plain
		.split("\n")
		.filter((l) => {
			const t = l.trim()
			if (!t) return false
			if (KNOWN_ISSUES.some((re) => re.test(t))) return false
			return (
				/unhandled|TypeError|ReferenceError|SyntaxError|Cannot find module|Error:/i.test(t) ||
				/exception/i.test(t) ||
				/\be=\b.*not found/i.test(t) ||
				/crashed|fatal|panic/i.test(t)
			)
		})
}

// ── PTY 키 시퀀스 ──

export const Key = {
	// 제어 문자
	ctrl: (ch: string) => {
		const code = ch.charCodeAt(0) - 96
		return String.fromCharCode(code < 0 ? code + 64 : code)
	},
	esc: "\x1b",
	enter: "\r",
	tab: "\t",
	backspace: "\x7f",
	delete: "\x1b[3~",
	// 화살표
	up: "\x1b[A",
	down: "\x1b[B",
	right: "\x1b[C",
	left: "\x1b[D",
	// 기능키
	f1: "\x1bOP",
	f2: "\x1bOQ",
	f3: "\x1bOR",
	f4: "\x1bOS",
	// 네비게이션
	home: "\x1b[H",
	end: "\x1b[F",
	pageUp: "\x1b[5~",
	pageDown: "\x1b[6~",
	// 수정자 조합
	ctrlUp: "\x1b[1;5A",
	ctrlDown: "\x1b[1;5B",
	ctrlRight: "\x1b[1;5C",
	ctrlLeft: "\x1b[1;5D",
	shiftUp: "\x1b[1;2A",
	shiftDown: "\x1b[1;2B",
	shiftRight: "\x1b[1;2C",
	shiftLeft: "\x1b[1;2D",
	shiftTab: "\x1b[Z",
	altA: "\x1ba",
	altE: "\x1be",
	altF: "\x1bf",
	altB: "\x1bb",
	altD: "\x1bd",
	// ctrl+조합
	ctrlA: "\x01",
	ctrlB: "\x02",
	ctrlC: "\x03",
	ctrlD: "\x04",
	ctrlE: "\x05",
	ctrlF: "\x06",
	ctrlG: "\x07",
	ctrlK: "\x0b",
	ctrlN: "\x0e",
	ctrlP: "\x10",
	ctrlR: "\x12",
	ctrlT: "\x14",
	ctrlU: "\x15",
	ctrlV: "\x16",
	ctrlW: "\x17",
	ctrlX: "\x18", // leader
	ctrlZ: "\x1a",
	// ctrl+shift
	ctrlShiftA: "\x1b[1;6A", // 실제로는 다를 수 있음 — 터미널에 따라 다름
	ctrlShiftD: "\x1b[1;6D",
	ctrlShiftE: "\x1b[1;6E",
	ctrlPageUp: "\x1b[5;5~",
	ctrlPageDown: "\x1b[6;5~",
	shiftF2: "\x1b[1;2Q",
}

// ── TUI 드라이버 ──

export class TuiDriver {
	pty: IPty
	output = ""
	snapshots: Array<{ name: string; raw: string; plain: string }> = []
	private exited = false

	constructor(cwd: string, env?: Record<string, string>) {
		this.pty = PtySpawn("node", [BIN, "tui"], {
			name: "xterm-256color",
			cols: 120,
			rows: 30,
			cwd,
			env: {
				...process.env,
				TERM: "xterm-256color",
				FORCE_COLOR: "1",
				SAEEOL_LLM_PROVIDER: "custom",
				SAEEOL_LLM_BASE_URL: "http://localhost:1",
				SAEEOL_LLM_API_KEY: "test",
				SAEEOL_LLM_MODEL: "test",
				...env,
			},
		})
		this.pty.onData((data) => {
			this.output += data
		})
		this.pty.onExit(() => {
			this.exited = true
		})
	}

	write(s: string) {
		this.pty.write(s)
	}

	async wait(ms: number) {
		await Bun.sleep(ms)
	}

	snapshot(name: string) {
		const raw = this.output
		const plain = stripAnsi(this.output)
		this.snapshots.push({ name, raw, plain })
		return { raw, plain }
	}

	errors(): string[] {
		return hasError(stripAnsi(this.output))
	}

	/** 마지막 스냅샷 이후 새로 발생한 에러만 */
	newErrors(): string[] {
		const prev = this.snapshots.length >= 2
			? stripAnsi(this.snapshots[this.snapshots.length - 2].raw)
			: ""
		const curr = stripAnsi(this.output)
		if (!prev) return hasError(curr)
		// prev에 없던 에러 라인만
		const prevSet = new Set(prev.split("\n").map((l) => l.trim()))
		return hasError(curr).filter((l) => !prevSet.has(l.trim()))
	}

	async kill() {
		try {
			this.pty.kill()
		} catch {}
	}

	get isExited() {
		return this.exited
	}

	/** 특정 텍스트가 화면에 나타날 때까지 대기 */
	async waitFor(text: string, timeoutMs = 10000): Promise<boolean> {
		const start = Date.now()
		while (Date.now() - start < timeoutMs) {
			if (stripAnsi(this.output).includes(text)) return true
			await Bun.sleep(200)
		}
		return false
	}

	/** leader 키 (ctrl+x) + 후속 키 */
	async leader(key: string, delay = 100) {
		this.write(Key.ctrlX)
		await this.wait(delay)
		this.write(key)
	}

	/** 에스케이프로 다이얼로그 닫기 */
	async dismiss(delay = 500) {
		this.write(Key.esc)
		await this.wait(delay)
	}

	/** 슬래시 명령어 실행 */
	async slash(cmd: string, delay = 500) {
		this.write(`/${cmd}`)
		await this.wait(delay)
		this.write(Key.enter)
		await this.wait(1500)
	}

	/** 스냅샷 리포트 저장 */
	async saveReport(filename: string) {
		const reportPath = path.join(import.meta.dir, filename)
		const report = this.snapshots
			.map(
				(s) =>
					`=== ${s.name} (${s.raw.length} bytes) ===\n${s.plain.slice(-500)}`,
			)
			.join("\n\n")
		await Bun.write(reportPath, report)
	}
}
