import { entryToFixture, loadFixtureFile, loadFixturesFromDir, normalizeResponse, validateFixtures } from "./fixture-loader.js";
import { falJobs } from "./fal-audio.js";
import { falQueueStates, imageResponseToFalJson, videoResponseToFalJson } from "./fal.js";
import { createServer } from "./server.js";

//#region src/llmock.ts
var LLMock = class LLMock {
	fixtures = [];
	searchFixtures = [];
	rerankFixtures = [];
	moderationFixtures = [];
	mounts = [];
	serverInstance = null;
	options;
	constructor(options) {
		this.options = options ?? {};
	}
	addFixture(fixture) {
		this.fixtures.push(fixture);
		return this;
	}
	addFixtures(fixtures) {
		this.fixtures.push(...fixtures);
		return this;
	}
	prependFixture(fixture) {
		this.fixtures.unshift(fixture);
		return this;
	}
	getFixtures() {
		return this.fixtures;
	}
	loadFixtureFile(filePath) {
		this.fixtures.push(...loadFixtureFile(filePath));
		return this;
	}
	loadFixtureDir(dirPath) {
		this.fixtures.push(...loadFixturesFromDir(dirPath));
		return this;
	}
	/**
	* Add fixtures from a JSON string or pre-parsed array of fixture entries.
	* Validates all fixtures and throws if any have severity "error".
	*/
	addFixturesFromJSON(input) {
		let entries;
		if (typeof input === "string") try {
			entries = JSON.parse(input);
		} catch (err) {
			throw new Error(`addFixturesFromJSON: invalid JSON — ${err instanceof Error ? err.message : String(err)}`);
		}
		else entries = input;
		const converted = entries.map((e) => entryToFixture(e));
		const errors = validateFixtures(converted).filter((i) => i.severity === "error");
		if (errors.length > 0) throw new Error(`Fixture validation failed: ${JSON.stringify(errors)}`);
		this.fixtures.push(...converted);
		return this;
	}
	clearFixtures() {
		this.fixtures.length = 0;
		return this;
	}
	on(match, response, opts) {
		return this.addFixture({
			match,
			response: typeof response === "function" ? response : normalizeResponse(response),
			...opts
		});
	}
	onMessage(pattern, response, opts) {
		return this.on({ userMessage: pattern }, response, opts);
	}
	onEmbedding(pattern, response, opts) {
		return this.on({ inputText: pattern }, response, opts);
	}
	onJsonOutput(pattern, jsonContent, opts) {
		const content = typeof jsonContent === "string" ? jsonContent : JSON.stringify(jsonContent);
		return this.on({
			userMessage: pattern,
			responseFormat: "json_object"
		}, { content }, opts);
	}
	onToolCall(name, response, opts) {
		return this.on({ toolName: name }, response, opts);
	}
	onToolResult(id, response, opts) {
		return this.on({ toolCallId: id }, response, opts);
	}
	onTurn(turn, pattern, response, opts) {
		return this.on({
			userMessage: pattern,
			turnIndex: turn
		}, response, opts);
	}
	onImage(prompt, response) {
		return this.addFixture({
			match: {
				userMessage: prompt,
				endpoint: "image"
			},
			response
		});
	}
	onSpeech(input, response) {
		return this.addFixture({
			match: {
				userMessage: input,
				endpoint: "speech"
			},
			response
		});
	}
	onTranscription(response) {
		return this.addFixture({
			match: { endpoint: "transcription" },
			response
		});
	}
	onTranslation(response) {
		return this.addFixture({
			match: { endpoint: "translation" },
			response
		});
	}
	onVideo(prompt, response) {
		return this.addFixture({
			match: {
				userMessage: prompt,
				endpoint: "video"
			},
			response
		});
	}
	onAudio(input, response) {
		return this.addFixture({
			match: { userMessage: input },
			response
		});
	}
	onSoundEffect(text, response) {
		return this.addFixture({
			match: {
				userMessage: text,
				endpoint: "audio-gen"
			},
			response
		});
	}
	onMusic(prompt, response) {
		return this.addFixture({
			match: {
				userMessage: prompt,
				endpoint: "audio-gen"
			},
			response
		});
	}
	onElevenLabsTTS(text, response) {
		return this.addFixture({
			match: {
				userMessage: text,
				endpoint: "elevenlabs-tts"
			},
			response
		});
	}
	onFalAudio(prompt, response, model) {
		return this.addFixture({
			match: {
				userMessage: prompt,
				endpoint: "fal-audio",
				...model ? { model } : {}
			},
			response
		});
	}
	onFalQueue(modelOrPrompt, response, opts) {
		const { billableUnits, ...fixtureOpts } = opts ?? {};
		return this.addFixture({
			match: {
				model: modelOrPrompt,
				endpoint: "fal"
			},
			response: {
				json: response,
				...billableUnits != null ? { billableUnits } : {}
			},
			...fixtureOpts
		});
	}
	onFalRun(modelOrPrompt, response, opts) {
		return this.onFalQueue(modelOrPrompt, response, opts);
	}
	/**
	* Register a fal.ai image fixture. Wraps an `ImageResponse` (the shape used
	* by `onImage` and OpenAI/Azure image fixtures) into fal's image envelope
	* before storing it as a `RawJSONResponse`. Defaults `width`/`height` to
	* 1024 when the fixture's `ImageItem` doesn't carry them.
	*/
	onFalImage(modelOrPrompt, response, opts) {
		return this.onFalQueue(modelOrPrompt, imageResponseToFalJson(response), opts);
	}
	/**
	* Register a fal.ai video fixture. Wraps a `VideoResponse` into fal's video
	* envelope (`{ video: { url, content_type, file_name, file_size }, seed }`)
	* before storing it as a `RawJSONResponse`.
	*/
	onFalVideo(modelOrPrompt, response, opts) {
		return this.onFalQueue(modelOrPrompt, videoResponseToFalJson(response), opts);
	}
	onSearch(pattern, results) {
		this.searchFixtures.push({
			match: pattern,
			results
		});
		return this;
	}
	onRerank(pattern, results) {
		this.rerankFixtures.push({
			match: pattern,
			results
		});
		return this;
	}
	onModerate(pattern, result) {
		this.moderationFixtures.push({
			match: pattern,
			result
		});
		return this;
	}
	/**
	* Queue a one-shot error that will be returned for the next matching
	* request, then automatically removed. Implemented as an internal fixture
	* with a `predicate` that always matches (so it fires first) and spliced
	* at the front of the fixture list.
	*/
	nextRequestError(status, errorBody) {
		const errorResponse = {
			error: {
				message: errorBody?.message ?? "Injected error",
				type: errorBody?.type ?? "server_error",
				code: errorBody?.code
			},
			status
		};
		const errorEndpointCompatible = (req) => {
			const reqEndpoint = req._endpointType;
			if (reqEndpoint === void 0 || reqEndpoint === "chat" || reqEndpoint === "embedding" || reqEndpoint.startsWith("realtime") || reqEndpoint === "fal") return true;
			return false;
		};
		const fixture = {
			match: { predicate: errorEndpointCompatible },
			response: errorResponse
		};
		this.fixtures.unshift(fixture);
		const original = fixture.match.predicate;
		fixture.match.predicate = (req) => {
			const result = original(req);
			if (result) {
				const idx = this.fixtures.indexOf(fixture);
				if (idx !== -1) this.fixtures.splice(idx, 1);
			}
			return result;
		};
		return this;
	}
	mount(path, handler) {
		this.mounts.push({
			path,
			handler
		});
		if (this.serverInstance) {
			if (handler.setJournal) handler.setJournal(this.serverInstance.journal);
			if (handler.setBaseUrl) handler.setBaseUrl(this.serverInstance.url + path);
			const registry = this.serverInstance.defaults.registry;
			if (registry && handler.setRegistry) handler.setRegistry(registry);
		}
		return this;
	}
	getRequests() {
		return this.journal.getAll();
	}
	getLastRequest() {
		return this.journal.getLast();
	}
	clearRequests() {
		this.journal.clear();
	}
	resetMatchCounts(testId) {
		if (this.serverInstance) this.serverInstance.journal.clearMatchCounts(testId);
		return this;
	}
	setChaos(config) {
		this.options.chaos = config;
		return this;
	}
	clearChaos() {
		delete this.options.chaos;
		return this;
	}
	enableRecording(config) {
		this.options.record = config;
		return this;
	}
	disableRecording() {
		delete this.options.record;
		return this;
	}
	reset() {
		this.clearFixtures();
		this.searchFixtures.length = 0;
		this.rerankFixtures.length = 0;
		this.moderationFixtures.length = 0;
		falJobs.clear();
		falQueueStates.clear();
		if (this.serverInstance) {
			this.serverInstance.journal.clear();
			this.serverInstance.videoStates.clear();
			this.serverInstance.openRouterVideoJobs.clear();
		}
		return this;
	}
	async start() {
		if (this.serverInstance) throw new Error("Server already started");
		this.serverInstance = await createServer(this.fixtures, this.options, this.mounts, {
			search: this.searchFixtures,
			rerank: this.rerankFixtures,
			moderation: this.moderationFixtures
		});
		return this.serverInstance.url;
	}
	async stop() {
		if (!this.serverInstance) throw new Error("Server not started");
		const { server } = this.serverInstance;
		await new Promise((resolve, reject) => {
			server.close((err) => err ? reject(err) : resolve());
		});
		this.serverInstance = null;
	}
	get journal() {
		if (!this.serverInstance) throw new Error("Server not started");
		return this.serverInstance.journal;
	}
	get url() {
		if (!this.serverInstance) throw new Error("Server not started");
		return this.serverInstance.url;
	}
	get baseUrl() {
		return this.url;
	}
	get port() {
		const parsed = new URL(this.url);
		if (!parsed.port) throw new Error(`Server URL has no explicit port: ${this.url}`);
		return parseInt(parsed.port, 10);
	}
	static async create(options) {
		const instance = new LLMock(options);
		await instance.start();
		return instance;
	}
};

//#endregion
export { LLMock };
//# sourceMappingURL=llmock.js.map