/**
 * plugin-contract.test.ts — 플러그인 시스템 컨슈머 계약 테스트
 *
 * closebook: consumer-contract
 *
 * 플러그인 인터페이스 계약 검증:
 * - Plugin 함수가 올바른 Hooks 객체를 반환하는지
 * - Hooks 인터페이스가 각 hook의 시그니처를 만족하는지
 * - PluginInput 컨텍스트가 기대하는 필드를 제공하는지
 * - 계약 파일(JSON) 직렬화 및 검증
 */

import { describe, expect, test } from "bun:test"
import { z } from "zod"
import { Hono } from "hono"
import type { Plugin, Hooks, PluginInput, AuthHook, ProviderHook } from "@saeeol/plugin"
import { tool } from "@saeeol/plugin"

// ── 계약 스키마 정의 ──

const HooksSchema = z.object({
	event: z.unknown().optional(),
	config: z.unknown().optional(),
	tool: z.record(z.string(), z.unknown()).optional(),
	auth: z
		.object({
			provider: z.string(),
			methods: z.array(z.unknown()),
		})
		.optional(),
	provider: z
		.object({
			id: z.string(),
		})
		.optional(),
})

const PluginInputSchema = z.object({
	client: z.unknown(),
	project: z.unknown(),
	directory: z.string(),
	worktree: z.string(),
	serverUrl: z.instanceof(URL),
	experimental_workspace: z.object({
		register: z.unknown(),
	}),
})

// ── 모의 Hono 앱 생성 (인터랙션 기반) ──

function createMockApp(interactions: Array<{ method: string; path: string; body?: unknown; status: number; response: unknown }>) {
	const app = new Hono()
	for (const ix of interactions) {
		app.on(ix.method, ix.path, (c) => c.json(ix.response, ix.status as 200))
	}
	return app
}

// ── 계약 정의 헬퍼 ──

interface ContractInteraction {
	state?: string
	description: string
	request: { method: string; path: string; headers?: Record<string, string>; body?: unknown }
	response: { status: number; body?: unknown }
}

interface PluginContract {
	consumer: string
	provider: string
	interactions: ContractInteraction[]
}

function defineContract(spec: PluginContract) {
	return {
		spec,
		serialize(): string {
			return JSON.stringify(
				{
					consumer: { name: spec.consumer },
					provider: { name: spec.provider },
					interactions: spec.interactions.map((ix) => ({
						description: ix.description,
						providerStates: ix.state ? [{ name: ix.state }] : [],
						request: ix.request,
						response: ix.response,
					})),
					metadata: { version: "1.0.0" },
				},
				null,
				2,
			)
		},
	}
}

// ── 테스트 ──

describe("plugin consumer contract: Plugin function signature", () => {
	test("plugin function returns valid Hooks object", async () => {
		const mock: Plugin = async () => ({
			event: async () => {},
			config: async () => {},
		})
		const hooks = await mock({} as PluginInput)
		const parsed = HooksSchema.safeParse(hooks)
		expect(parsed.success).toBe(true)
		if (!parsed.success) {
			console.error("Hooks contract violations:", parsed.error.issues)
		}
	})

	test("plugin with tool hook returns valid Hooks", async () => {
		const mock: Plugin = async () => ({
			tool: {
				example: tool({
					description: "example tool",
					args: { input: z.string() },
					async execute(args, ctx) {
						return args.input
					},
				}),
			},
		})
		const hooks = await mock({} as PluginInput)
		expect(hooks.tool).toBeDefined()
		expect(hooks.tool?.example).toBeDefined()
		expect(hooks.tool?.example.description).toBe("example tool")
	})

	test("plugin with auth hook returns valid Hooks", async () => {
		const authHook: AuthHook = {
			provider: "test-provider",
			methods: [
				{
					type: "api",
					label: "Test Auth",
					async authorize() {
						return { type: "success", key: "test-key" }
					},
				},
			],
		}
		const mock: Plugin = async () => ({ auth: authHook })
		const hooks = await mock({} as PluginInput)
		expect(hooks.auth).toBeDefined()
		expect(hooks.auth?.provider).toBe("test-provider")
		expect(hooks.auth?.methods.length).toBe(1)
	})

	test("plugin with provider hook returns valid Hooks", async () => {
		const providerHook: ProviderHook = {
			id: "test-provider",
		}
		const mock: Plugin = async () => ({ provider: providerHook })
		const hooks = await mock({} as PluginInput)
		expect(hooks.provider).toBeDefined()
		expect(hooks.provider?.id).toBe("test-provider")
	})
})

describe("plugin consumer contract: PluginInput shape", () => {
	test("PluginInput schema validates required fields", () => {
		const input = {
			client: {},
			project: {},
			directory: "/tmp/test",
			worktree: "/tmp/test",
			serverUrl: new URL("http://localhost:4096"),
			experimental_workspace: {
				register: () => {},
			},
			$: undefined,
		}
		const parsed = PluginInputSchema.safeParse(input)
		expect(parsed.success).toBe(true)
		if (!parsed.success) {
			console.error("PluginInput contract violations:", parsed.error.issues)
		}
	})
})

describe("plugin consumer contract: trigger hook contracts", () => {
	test("chat.message hook receives correct input/output shape", async () => {
		const received: unknown[] = []
		const mock: Plugin = async () => ({
			"chat.message": async (input, output) => {
				received.push({ input, output })
			},
		})
		const hooks = await mock({} as PluginInput)

		const testInput = {
			sessionID: "sess_123",
			agent: "build",
			model: { providerID: "test", modelID: "test-model" },
		}
		const testOutput = {
			message: { id: "msg_1", role: "user" as const },
			parts: [],
		}
		await hooks["chat.message"]?.(testInput as any, testOutput as any)
		expect(received.length).toBe(1)
		expect((received[0] as any).input.sessionID).toBe("sess_123")
	})

	test("chat.params hook can modify output", async () => {
		const mock: Plugin = async () => ({
			"chat.params": async (_input, output) => {
				output.temperature = 0.5
			},
		})
		const hooks = await mock({} as PluginInput)

		const output = { temperature: 1.0, topP: 1.0, topK: 40, maxOutputTokens: undefined, options: {} }
		await hooks["chat.params"]?.({} as any, output as any)
		expect(output.temperature).toBe(0.5)
	})

	test("shell.env hook can inject environment variables", async () => {
		const mock: Plugin = async () => ({
			"shell.env": async (_input, output) => {
				output.env.CUSTOM_VAR = "injected"
			},
		})
		const hooks = await mock({} as PluginInput)

		const output = { env: {} as Record<string, string> }
		await hooks["shell.env"]?.({ cwd: "/tmp" } as any, output as any)
		expect(output.env.CUSTOM_VAR).toBe("injected")
	})

	test("permission.ask hook can override permission decision", async () => {
		const mock: Plugin = async () => ({
			"permission.ask": async (_input, output) => {
				output.status = "allow"
			},
		})
		const hooks = await mock({} as PluginInput)

		const output = { status: "ask" as "ask" | "deny" | "allow" }
		await hooks["permission.ask"]?.({} as any, output)
		expect(output.status).toBe("allow")
	})
})

describe("plugin consumer contract: pact artifact generation", () => {
	test("generates valid contract JSON from plugin interactions", async () => {
		const contract = defineContract({
			consumer: "saeeol-server",
			provider: "saeeol-plugin-interface",
			interactions: [
				{
					state: "plugin loaded",
					description: "plugin returns hooks with event callback",
					request: { method: "POST", path: "/plugin/init", body: {} },
					response: { status: 200, body: { event: "[function]" } },
				},
				{
					description: "trigger chat.message hook",
					request: { method: "POST", path: "/plugin/hook/chat.message", body: { sessionID: "test" } },
					response: { status: 200, body: {} },
				},
			],
		})

		const serialized = contract.serialize()
		const parsed = JSON.parse(serialized)
		expect(parsed.consumer.name).toBe("saeeol-server")
		expect(parsed.provider.name).toBe("saeeol-plugin-interface")
		expect(parsed.interactions.length).toBe(2)
		expect(parsed.metadata.version).toBe("1.0.0")
	})

	test("mock app serves contract interactions correctly", async () => {
		const mockApp = createMockApp([
			{ method: "GET", path: "/api/test", status: 200, response: { ok: true } },
			{ method: "POST", path: "/api/create", status: 201, response: { id: "new" } },
		])

		const getRes = await mockApp.request("/api/test")
		const getBody = await getRes.json()
		expect(getRes.status).toBe(200)
		expect(getBody).toEqual({ ok: true })

		const postRes = await mockApp.request("/api/create", { method: "POST" })
		const postBody = await postRes.json()
		expect(postRes.status).toBe(201)
		expect(postBody).toEqual({ id: "new" })
	})
})
