/**
 * contract.test.ts — SAEEOL 서버 계약 테스트
 *
 * closebook 패턴 기반:
 * - response-conformance: Zod 스키마로 응답 바디 적합성 검사
 * - consumer-contract: SDK/클라이언트가 기대하는 API 응답 스키마 검증
 * - stateful-api-testing: 세션 생성 → 조회 → 수정 → 삭제 순차 테스트
 *
 * Hono app.request()로 실제 HTTP 서버 없이 테스트.
 * Bun test + Zod 검증.
 */

import { afterAll, afterEach, describe, expect, test } from "bun:test"
import { Flag } from "@saeeol/core/flag/flag"
import { Server } from "../../src/server/server"
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
import * as Log from "@saeeol/core/util/log"
import { z } from "zod"
import { checkHonoResponse } from "./lib/conformance"
import { runSequence } from "./lib/stateful"

void Log.init({ print: false })

const original = Flag.SAEEOL_EXPERIMENTAL_HTTPAPI

function app(experimental = true) {
	Flag.SAEEOL_EXPERIMENTAL_HTTPAPI = experimental
	return experimental ? Server.Default().app : Server.Legacy().app
}

// ── Zod 스키마 (계약: 클라이언트가 기대하는 응답 형태) ──

const HealthSchema = z.object({
	healthy: z.boolean(),
	version: z.string(),
})

const ConfigSchema = z
	.object({
		model: z.string().optional(),
		username: z.string().optional(),
	})
	.passthrough()

const SessionListSchema = z.array(
	z
		.object({
			id: z.string(),
			title: z.string().optional(),
			createdAt: z.number().optional(),
		})
		.passthrough(),
)

const SessionInfoSchema = z
	.object({
		id: z.string(),
	})
	.passthrough()

const AgentListSchema = z.array(z.unknown())

const PathSchema = z.object({
	home: z.string(),
	state: z.string(),
	config: z.string(),
	worktree: z.string(),
	directory: z.string(),
})

const SkillListSchema = z.array(z.unknown())

const CommandListSchema = z.array(z.unknown())

// ── 공통 정리 ──

const cleanupQueue: Array<() => Promise<void>> = []

afterAll(async () => {
	Flag.SAEEOL_EXPERIMENTAL_HTTPAPI = original
	await disposeAllInstances()
	await Bun.sleep(500)
	for (const fn of cleanupQueue) await fn()
})

// ── response-conformance: 라우트별 응답 적합성 검증 ──

describe("response conformance: server routes", () => {
	describe("GET /global/health", () => {
		test("legacy: matches HealthSchema", async () => {
			const res = await app(false).request("/global/health")
			const result = await checkHonoResponse(res, HealthSchema, [200])
			expect(result.ok).toBe(true)
			if (!result.ok) console.error("Health violations:", result.violations)
		})

		test("experimental: matches HealthSchema", async () => {
			const res = await app(true).request("/global/health")
			const result = await checkHonoResponse(res, HealthSchema, [200])
			expect(result.ok).toBe(true)
			if (!result.ok) console.error("Health violations:", result.violations)
		})
	})

	describe("GET /global/config", () => {
		test("legacy: matches ConfigSchema", async () => {
			await using tmp = await tmpdir({ config: {} })
			const res = await app(false).request("/global/config", {
				headers: { "x-saeeol-directory": tmp.path },
			})
			const result = await checkHonoResponse(res, ConfigSchema, [200])
			expect(result.ok).toBe(true)
			if (!result.ok) console.error("Config violations:", result.violations)
		})
	})

	describe("GET /agent", () => {
		test("legacy: returns agent list", async () => {
			await using tmp = await tmpdir({ config: {} })
			const res = await app(false).request("/agent", {
				headers: { "x-saeeol-directory": tmp.path },
			})
			const result = await checkHonoResponse(res, AgentListSchema, [200])
			expect(result.ok).toBe(true)
			if (!result.ok) console.error("Agent violations:", result.violations)
		})
	})

	describe("GET /path", () => {
		test("legacy: returns path info", async () => {
			const tmp = await tmpdir({ git: true, config: {} })
			cleanupQueue.push(() => tmp[Symbol.asyncDispose]())
			const res = await app(false).request("/path", {
				headers: { "x-saeeol-directory": tmp.path },
			})
			const result = await checkHonoResponse(res, PathSchema, [200])
			expect(result.ok).toBe(true)
			if (!result.ok) console.error("Path violations:", result.violations)
		})
	})

	describe("GET /skill", () => {
		test("legacy: returns skill list", async () => {
			const tmp = await tmpdir({ git: true, config: {} })
			cleanupQueue.push(() => tmp[Symbol.asyncDispose]())
			const res = await app(false).request("/skill", {
				headers: { "x-saeeol-directory": tmp.path },
			})
			const result = await checkHonoResponse(res, SkillListSchema, [200])
			expect(result.ok).toBe(true)
			if (!result.ok) console.error("Skill violations:", result.violations)
		})
	})

	describe("GET /command", () => {
		test("legacy: returns command list", async () => {
			const tmp = await tmpdir({ config: {} })
			cleanupQueue.push(() => tmp[Symbol.asyncDispose]())
			const res = await app(false).request("/command", {
				headers: { "x-saeeol-directory": tmp.path },
			})
			const result = await checkHonoResponse(res, CommandListSchema, [200])
			expect(result.ok).toBe(true)
			if (!result.ok) console.error("Command violations:", result.violations)
		})
	})
})

// ── stateful-api-testing: 세션 라이프사이클 순차 테스트 ──

describe("stateful: session lifecycle", () => {
	test("create → list → get → update → delete", async () => {
		const tmp = await tmpdir({ git: true, config: {} })
		cleanupQueue.push(() => tmp[Symbol.asyncDispose]())
		Flag.SAEEOL_EXPERIMENTAL_HTTPAPI = false
		const headers = { "x-saeeol-directory": tmp.path }

		const result = await runSequence({
			app: app(false),
			headers,
			steps: [
				{
					name: "create session",
					request: { method: "POST", path: "/session", body: { title: "stateful-test" } },
					extract: { sessionId: (body: unknown) => (body as Record<string, unknown>).id },
					validate: { status: 200, body: SessionInfoSchema },
				},
				{
					name: "list sessions",
					request: { method: "GET", path: "/session" },
					validate: { status: 200, body: SessionListSchema },
				},
				{
					name: "get session",
					request: { method: "GET", path: "/session/{sessionId}" },
					validate: { status: 200, body: SessionInfoSchema },
				},
				{
					name: "update session title",
					request: {
						method: "PATCH",
						path: "/session/{sessionId}",
						body: { title: "updated-title" },
					},
					validate: { status: 200, body: SessionInfoSchema },
				},
				{
					name: "delete session",
					request: { method: "DELETE", path: "/session/{sessionId}" },
					validate: { status: 200, body: z.boolean() },
				},
			],
		})

		expect(result.ok).toBe(true)
		expect(result.captured.sessionId).toBeTruthy()

		if (!result.ok) {
			for (const step of result.steps) {
				if (!step.ok) {
					console.error(`FAIL [${step.step}]:`, step.violations)
				}
			}
		}
	})

	test("create → list contains new session", async () => {
		const tmp = await tmpdir({ git: true, config: {} })
		cleanupQueue.push(() => tmp[Symbol.asyncDispose]())
		Flag.SAEEOL_EXPERIMENTAL_HTTPAPI = false
		const headers = { "x-saeeol-directory": tmp.path }

		const result = await runSequence({
			app: app(false),
			headers,
			steps: [
				{
					name: "create session",
					request: { method: "POST", path: "/session", body: {} },
					extract: { sessionId: (body: unknown) => (body as Record<string, unknown>).id },
					validate: { status: 200 },
				},
				{
					name: "list sessions",
					request: { method: "GET", path: "/session" },
					validate: { status: 200, body: SessionListSchema },
				},
			],
		})

		expect(result.ok).toBe(true)
		expect(result.captured.sessionId).toBeTruthy()
		const listBody = result.steps[1].body as unknown[]
		expect(listBody.length).toBeGreaterThanOrEqual(1)
	})

	test("create → get → children (empty)", async () => {
		const tmp = await tmpdir({ git: true, config: {} })
		cleanupQueue.push(() => tmp[Symbol.asyncDispose]())
		Flag.SAEEOL_EXPERIMENTAL_HTTPAPI = false
		const headers = { "x-saeeol-directory": tmp.path }

		const result = await runSequence({
			app: app(false),
			headers,
			steps: [
				{
					name: "create session",
					request: { method: "POST", path: "/session", body: {} },
					extract: { sessionId: (body: unknown) => (body as Record<string, unknown>).id },
					validate: { status: 200 },
				},
				{
					name: "get children",
					request: { method: "GET", path: "/session/{sessionId}/children" },
					validate: { status: 200, body: z.array(z.unknown()) },
				},
			],
		})

		expect(result.ok).toBe(true)
	})
})

// ── response-conformance: 에러 응답 검증 ──

describe("response conformance: error routes", () => {
	test("session get with invalid ID returns error", async () => {
		const tmp = await tmpdir({ git: true, config: {} })
		cleanupQueue.push(() => tmp[Symbol.asyncDispose]())
		const res = await app(false).request("/session/nonexistent-id", {
			headers: { "x-saeeol-directory": tmp.path },
		})
		expect([400, 404]).toContain(res.status)
	})
})

// ── consumer-contract: 계약 파일 생성 ──

describe("consumer contract: pact artifact generation", () => {
	afterEach(async () => {
		Flag.SAEEOL_EXPERIMENTAL_HTTPAPI = original
		await disposeAllInstances()
	})

	test("collects verified interactions into contract", async () => {
		await using tmp = await tmpdir({ config: {} })
		const headers = { "x-saeeol-directory": tmp.path }
		const interactions = []

		// Health 인터랙션
		const healthRes = await app(false).request("/global/health")
		const healthBody = await healthRes.json()
		interactions.push({
			description: "health check",
			providerStates: [],
			request: { method: "GET", path: "/global/health" },
			response: { status: healthRes.status, body: healthBody },
		})

		// Config 인터랙션
		const configRes = await app(false).request("/global/config", { headers })
		const configBody = await configRes.json()
		interactions.push({
			description: "global config",
			providerStates: [],
			request: { method: "GET", path: "/global/config" },
			response: { status: configRes.status, body: configBody },
		})

		// Agent 인터랙션
		const agentRes = await app(false).request("/agent", { headers })
		const agentBody = await agentRes.json()
		interactions.push({
			description: "agent list",
			providerStates: [],
			request: { method: "GET", path: "/agent" },
			response: { status: agentRes.status, body: agentBody },
		})

		// 계약 파일 검증
		const pact = {
			consumer: { name: "saeeol-vscode" },
			provider: { name: "saeeol-server" },
			interactions,
			metadata: { version: "1.0.0" },
		}

		expect(pact.interactions.length).toBe(3)
		for (const ix of pact.interactions) {
			expect(ix).toHaveProperty("description")
			expect(ix).toHaveProperty("request")
			expect(ix).toHaveProperty("response")
			expect(ix.response.status).toBeLessThan(500)
		}
	})
})
