/**
 * tui-walkthrough.test.ts — SAEEOL TUI 전체 워크스루
 *
 * bun-pty로 TUI를 띄우고 모든 화면/단축키를 자동으로 눌러서:
 * 1. 에러 없이 렌더링되는지
 * 2. 각 다이얼로그가 열리고 닫히는지
 * 3. 슬래시 명령이 동작하는지
 * 4. 화면 전환이 정상인지
 * 5. 세션 생성 → 메시지 흐름이 되는지
 *
 * API 호출/비용 발생 없음 — LLM 엔드포인트는 죽은 서버로 설정.
 */

import { describe, expect, test, beforeAll, afterAll } from "bun:test"
import { spawn as PtySpawn, type IPty } from "bun-pty"
import { tmpdir, disposeAllInstances } from "../fixture/fixture"
import * as Log from "@saeeol/core/util/log"
import { Flag } from "@saeeol/core/flag/flag"
import path from "path"

void Log.init({ print: false })

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

// ── 유틸리티 ──

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,
]

function hasError(plain: string): string[] {
	return plain
		.split("\n")
		.filter((l) => {
			const t = l.trim()
			if (!t) return false
			// known issue면 무조건 스킵
			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)
			)
		})
}

// ── TUI 드라이버 ──

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

	constructor(cwd: 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",
			},
		})
		this.pty.onData((data) => {
			this.output += data
		})
	}

	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))
	}

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

	/** 특정 텍스트가 화면에 나타날 때까지 대기 */
	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("\x18") // ctrl+x
		await this.wait(delay)
		this.write(key)
	}

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

// ── 테스트 ──

describe("TUI walkthrough: full interaction", () => {
	let driver: TuiDriver
	let tmp: Awaited<ReturnType<typeof tmpdir>>

	beforeAll(async () => {
		tmp = await tmpdir({ git: true, config: {} })
		Flag.SAEEOL_EXPERIMENTAL_HTTPAPI = false
	})

	afterAll(async () => {
		await driver?.kill()
		await disposeAllInstances()
		Flag.SAEEOL_EXPERIMENTAL_HTTPAPI = true
	})

	// ── 1. 부트스트랩 ──

	test("boot: TUI starts without errors", async () => {
		driver = new TuiDriver(tmp.path)

		// 서버 시작 + 초기 렌더링 대기
		await driver.wait(6000)
		driver.snapshot("boot")

		// 에러 없음
		expect(driver.errors()).toHaveLength(0)

		// alternate screen 진입
		expect(driver.output).toMatch(/\x1b\[\?9001h|\x1b\[\?1004h/)
	})

	// ── 2. Home 화면 ──

	test("home: renders content", async () => {
		const { plain } = driver.snapshot("home")
		expect(plain.length).toBeGreaterThan(0)
		expect(driver.errors()).toHaveLength(0)
	})

	// ── 3. 커맨드 팔레트 (ctrl+p) ──

	test("command palette: ctrl+p opens, esc closes", async () => {
		driver.write("\x10") // ctrl+p
		await driver.wait(1000)
		driver.snapshot("command-palette-open")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
		driver.snapshot("command-palette-closed")
		expect(driver.errors()).toHaveLength(0)
	})

	// ── 4. 세션 리스트 (leader+l) ──

	test("session list: leader+l opens dialog", async () => {
		await driver.leader("l")
		await driver.wait(1500)
		driver.snapshot("session-list")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	// ── 5. 모델 선택 (leader+m) ──

	test("model selector: leader+m opens dialog", async () => {
		await driver.leader("m")
		await driver.wait(1500)
		driver.snapshot("model-selector")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	// ── 6. 에이전트 선택 (leader+a) ──

	test("agent selector: leader+a opens dialog", async () => {
		await driver.leader("a")
		await driver.wait(1500)
		driver.snapshot("agent-selector")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	// ── 7. 상태 다이얼로그 (leader+s) ──

	test("status dialog: leader+s opens", async () => {
		await driver.leader("s")
		await driver.wait(1500)
		driver.snapshot("status")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	// ── 8. 테마 선택 (leader+t) ──

	test("theme selector: leader+t opens", async () => {
		await driver.leader("t")
		await driver.wait(1500)
		driver.snapshot("theme")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	// ── 9. 슬래시 명령어 ──

	test("slash /help: opens help dialog", async () => {
		driver.write("/help")
		await driver.wait(500)
		driver.write("\r") // enter
		await driver.wait(1500)
		driver.snapshot("slash-help")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	test("slash /status: opens status", async () => {
		driver.write("/status")
		await driver.wait(500)
		driver.write("\r")
		await driver.wait(1500)
		driver.snapshot("slash-status")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	test("slash /models: opens model dialog", async () => {
		driver.write("/models")
		await driver.wait(500)
		driver.write("\r")
		await driver.wait(1500)
		driver.snapshot("slash-models")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	test("slash /agents: opens agent dialog", async () => {
		driver.write("/agents")
		await driver.wait(500)
		driver.write("\r")
		await driver.wait(1500)
		driver.snapshot("slash-agents")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	test("slash /themes: opens theme dialog", async () => {
		driver.write("/themes")
		await driver.wait(500)
		driver.write("\r")
		await driver.wait(1500)
		driver.snapshot("slash-themes")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	test("slash /sessions: opens session list", async () => {
		driver.write("/sessions")
		await driver.wait(500)
		driver.write("\r")
		await driver.wait(1500)
		driver.snapshot("slash-sessions")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	test("slash /connect: opens provider dialog", async () => {
		driver.write("/connect")
		await driver.wait(500)
		driver.write("\r")
		await driver.wait(1500)
		driver.snapshot("slash-connect")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	test("slash /mcps: opens MCP dialog", async () => {
		driver.write("/mcps")
		await driver.wait(500)
		driver.write("\r")
		await driver.wait(1500)
		driver.snapshot("slash-mcps")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	test("slash /variants: opens variant dialog", async () => {
		driver.write("/variants")
		await driver.wait(500)
		driver.write("\r")
		await driver.wait(1500)
		driver.snapshot("slash-variants")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	test("slash /new: navigates home", async () => {
		driver.write("/new")
		await driver.wait(500)
		driver.write("\r")
		await driver.wait(1000)
		driver.snapshot("slash-new")
		expect(driver.errors()).toHaveLength(0)
	})

	// ── 10. 프롬프트 입력 ──

	test("prompt: typing and clearing", async () => {
		driver.write("hello world")
		await driver.wait(500)
		driver.snapshot("prompt-typed")
		expect(driver.errors()).toHaveLength(0)

		// ctrl+c로 클리어
		driver.write("\x03")
		await driver.wait(300)
		driver.snapshot("prompt-cleared")
		expect(driver.errors()).toHaveLength(0)
	})

	// ── 11. 새 세션 (leader+n) ──

	test("new session: leader+n navigates home", async () => {
		await driver.leader("n")
		await driver.wait(1000)
		driver.snapshot("new-session")
		expect(driver.errors()).toHaveLength(0)
	})

	// ── 12. 사이드바 토글 (leader+b) ──

	test("sidebar: leader+b toggles", async () => {
		await driver.leader("b")
		await driver.wait(800)
		driver.snapshot("sidebar-open")
		expect(driver.errors()).toHaveLength(0)

		// 다시 토글
		await driver.leader("b")
		await driver.wait(500)
		driver.snapshot("sidebar-closed")
		expect(driver.errors()).toHaveLength(0)
	})

	// ── 13. 스크롤 키 ──

	test("scroll: page up/down work", async () => {
		driver.write("\x1b[5~") // page up
		await driver.wait(300)
		driver.write("\x1b[6~") // page down
		await driver.wait(300)
		driver.snapshot("scroll")
		expect(driver.errors()).toHaveLength(0)
	})

	// ── 14. 모델 사이클 (f2) ──

	test("model cycle: f2 cycles models", async () => {
		// f2 키 시퀀스
		driver.write("\x1bOQ") // f2 (xterm)
		await driver.wait(500)
		driver.snapshot("model-cycle-f2")
		expect(driver.errors()).toHaveLength(0)
	})

	// ── 15. 에이전트 사이클 (tab) ──

	test("agent cycle: tab cycles agents", async () => {
		driver.write("\t") // tab
		await driver.wait(500)
		driver.snapshot("agent-cycle-tab")
		expect(driver.errors()).toHaveLength(0)
	})

	// ── 16. 세션 콤팩트 (leader+c) ──

	test("compact: leader+c triggers compact", async () => {
		await driver.leader("c")
		await driver.wait(1000)
		driver.snapshot("session-compact")
		expect(driver.errors()).toHaveLength(0)
	})

	// ── 17. 세션 타임라인 (leader+g) ──

	test("timeline: leader+g opens timeline", async () => {
		await driver.leader("g")
		await driver.wait(1000)
		driver.snapshot("session-timeline")
		expect(driver.errors()).toHaveLength(0)

		await driver.dismiss()
	})

	// ── 18. 커서 이동 키 ──

	test("cursor: arrow keys and home/end", async () => {
		// 입력창에 텍스트 입력 후 커서 이동
		driver.write("test input for cursor movement")
		await driver.wait(300)

		driver.write("\x1b[D") // left
		await driver.wait(100)
		driver.write("\x1b[C") // right
		await driver.wait(100)
		driver.write("\x1b[H") // home
		await driver.wait(100)
		driver.write("\x1b[F") // end
		await driver.wait(100)

		driver.snapshot("cursor-movement")
		expect(driver.errors()).toHaveLength(0)

		// 클리어
		driver.write("\x03")
		await driver.wait(300)
	})

	// ── 19. 슬래시 명령어 — 별칭 ──

	test("slash /quit: alias for /exit", async () => {
		driver.write("/quit")
		await driver.wait(500)
		driver.write("\r")
		await driver.wait(2000)
		driver.snapshot("slash-quit")

		// /quit이 /exit과 동일하게 동작하면 TUI가 종료되어야 함
		// 종료되면 에러 없이 프로세스가 끝남
		const exited = await Promise.race([
			new Promise<boolean>((resolve) => {
				driver.pty.onExit(() => resolve(true))
				setTimeout(() => resolve(false), 3000)
			}),
		])

		if (exited) {
			// 정상 종료 — 드라이버를 다시 만들 필요 없음 (이후 테스트 없음)
			expect(driver.errors()).toHaveLength(0)
		} else {
			// 종료 안 됨 — 에스케이프 후 계속
			await driver.dismiss()
			expect(driver.errors()).toHaveLength(0)
		}
	})

	// ── 20. 전체 스냅샷 검증 ──

	test("all snapshots: no errors in any screen", async () => {
		const allErrors: string[] = []
		for (const snap of driver.snapshots) {
			const snapErrors = hasError(snap.plain)
			if (snapErrors.length > 0) {
				allErrors.push(
					`[${snap.name}]: ${snapErrors.join("; ")}`,
				)
			}
		}

		// 디버그: 전체 스냅샷 로그 저장
		const reportPath = path.join(import.meta.dir, ".tui-walkthrough-report.txt")
		const report = driver.snapshots
			.map(
				(s) =>
					`=== ${s.name} (${s.raw.length} bytes) ===\n${s.plain.slice(-500)}`,
			)
			.join("\n\n")
		await Bun.write(reportPath, report)

		expect(allErrors).toHaveLength(0)
	})
})
