import { expect } from 'vitest';
import { formatExchangeContext, formatExchangeList } from './matcher-format.js';
function formatExecContext(result) {
    const lines = [];
    if (result.command) {
        lines.push(`  Command: ${result.command}`);
    }
    lines.push(`  Exit code: ${result.exitCode}`, `  stdout: ${result.stdout || '(empty)'}`, `  stderr: ${result.stderr || '(empty)'}`);
    return lines.join('\n');
}
function isObject(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function matchesDeepPartial(actual, expected) {
    if (Array.isArray(expected)) {
        if (!Array.isArray(actual)) {
            return false;
        }
        return expected.every((expectedItem, index) => matchesDeepPartial(actual[index], expectedItem));
    }
    if (isObject(expected)) {
        if (!isObject(actual)) {
            return false;
        }
        return Object.entries(expected).every(([key, expectedValue]) => matchesDeepPartial(actual[key], expectedValue));
    }
    return Object.is(actual, expected);
}
function truncate(value, maxLength) {
    if (value.length <= maxLength) {
        return value;
    }
    return `${value.slice(0, maxLength)}...`;
}
function messageContentToString(content) {
    if (typeof content === 'string') {
        return content;
    }
    if (content === undefined) {
        return '';
    }
    try {
        return JSON.stringify(content) ?? String(content);
    }
    catch {
        return String(content);
    }
}
function matchesContent(content, matcher) {
    if (typeof matcher === 'string') {
        return content.includes(matcher);
    }
    return new RegExp(matcher.source, matcher.flags).test(content);
}
expect.extend({
    toHaveExitCode(received, expected) {
        const pass = received.exitCode === expected;
        return {
            pass,
            message: () => pass
                ? `expected exit code not to be ${expected}\n${formatExecContext(received)}`
                : `expected exit code ${expected}, got ${received.exitCode}\n${formatExecContext(received)}`,
        };
    },
    toSucceedWith(received, text) {
        const exitCodeOk = received.exitCode === 0;
        const stdoutMatch = received.stdout.includes(text);
        const pass = exitCodeOk && stdoutMatch;
        return {
            pass,
            message: () => {
                if (pass) {
                    return `expected command not to succeed with "${text}"\n${formatExecContext(received)}`;
                }
                const reasons = [];
                if (!exitCodeOk)
                    reasons.push(`exit code was ${received.exitCode} (expected 0)`);
                if (!stdoutMatch)
                    reasons.push(`stdout does not contain "${text}"`);
                return `expected command to succeed with "${text}"\n  ${reasons.join(', ')}\n${formatExecContext(received)}`;
            },
        };
    },
    toFail(received) {
        const pass = received.exitCode !== 0;
        return {
            pass,
            message: () => pass
                ? `expected command not to fail\n${formatExecContext(received)}`
                : `expected command to fail but it exited with code 0\n${formatExecContext(received)}`,
        };
    },
    toFailWith(received, text) {
        const exitCodeFail = received.exitCode !== 0;
        const stderrMatch = received.stderr.includes(text);
        const pass = exitCodeFail && stderrMatch;
        return {
            pass,
            message: () => {
                if (pass) {
                    return `expected command not to fail with "${text}"\n${formatExecContext(received)}`;
                }
                const reasons = [];
                if (!exitCodeFail)
                    reasons.push('command succeeded (exit code 0)');
                if (!stderrMatch)
                    reasons.push(`stderr does not contain "${text}"`);
                return `expected command to fail with "${text}"\n  ${reasons.join(', ')}\n${formatExecContext(received)}`;
            },
        };
    },
    toHaveStdout(received, matcher) {
        const pass = typeof matcher === 'string'
            ? received.stdout.includes(matcher)
            : matcher.test(received.stdout);
        return {
            pass,
            message: () => pass
                ? `expected stdout not to match ${matcher}\n${formatExecContext(received)}`
                : `expected stdout to match ${matcher}\n${formatExecContext(received)}`,
        };
    },
    toHaveStderr(received, matcher) {
        const pass = typeof matcher === 'string'
            ? received.stderr.includes(matcher)
            : matcher.test(received.stderr);
        return {
            pass,
            message: () => pass
                ? `expected stderr not to match ${matcher}\n${formatExecContext(received)}`
                : `expected stderr to match ${matcher}\n${formatExecContext(received)}`,
        };
    },
    async toHaveFile(received, filePath) {
        const exists = await received.fileExists(filePath);
        return {
            pass: exists,
            message: () => exists
                ? `expected container not to have file "${filePath}"`
                : `expected container to have file "${filePath}"`,
        };
    },
    async toHaveFileContaining(received, filePath, text) {
        const exists = await received.fileExists(filePath);
        if (!exists) {
            return {
                pass: false,
                message: () => `expected file "${filePath}" to contain "${text}", but file does not exist`,
            };
        }
        const content = await received.readFile(filePath);
        const pass = content.includes(text);
        return {
            pass,
            message: () => pass
                ? `expected file "${filePath}" not to contain "${text}"\n  Content: ${content}`
                : `expected file "${filePath}" to contain "${text}"\n  Content: ${content}`,
        };
    },
    toHaveRequestBody(received, expected) {
        const actualBody = received.request.body;
        const pass = matchesDeepPartial(actualBody, expected);
        return {
            pass,
            message: () => {
                if (pass) {
                    return `expected request body not to match\n\nFull exchange:\n${formatExchangeContext(received)}`;
                }
                const diff = this.utils.diff(expected, actualBody, { expand: this.expand }) ??
                    '  (no diff available)';
                return `expected request body to match\n\nDiff:\n${diff}\n\nFull exchange:\n${formatExchangeContext(received)}`;
            },
        };
    },
    toHaveResponseBody(received, expected) {
        const actualBody = received.response.body;
        const pass = matchesDeepPartial(actualBody, expected);
        return {
            pass,
            message: () => {
                if (pass) {
                    return `expected response body not to match\n\nFull exchange:\n${formatExchangeContext(received)}`;
                }
                const diff = this.utils.diff(expected, actualBody, { expand: this.expand }) ??
                    '  (no diff available)';
                return `expected response body to match\n\nDiff:\n${diff}\n\nFull exchange:\n${formatExchangeContext(received)}`;
            },
        };
    },
    toContainRequest(received, matcher) {
        const match = received.find((exchange) => {
            if (matcher.path && !exchange.request.path.startsWith(matcher.path)) {
                return false;
            }
            if (matcher.method && exchange.request.method !== matcher.method) {
                return false;
            }
            if (matcher.bodyContaining &&
                !matchesDeepPartial(exchange.request.body, matcher.bodyContaining)) {
                return false;
            }
            return true;
        });
        const pass = match !== undefined;
        const criteria = [
            matcher.path ? `path: "${matcher.path}"` : null,
            matcher.method ? `method: "${matcher.method}"` : null,
            matcher.bodyContaining
                ? `body containing: ${JSON.stringify(matcher.bodyContaining)}`
                : null,
        ]
            .filter((value) => value !== null)
            .join(', ');
        return {
            pass,
            message: () => pass
                ? `expected no exchange matching {${criteria}}\n\nAll captured exchanges:\n${formatExchangeList(received)}`
                : `expected at least one exchange matching {${criteria}}, but none found\n\nAll captured exchanges (${received.length}):\n${formatExchangeList(received)}`,
        };
    },
    toHaveToolInRequest(received, toolName) {
        const requestBody = received.request.body;
        const toolNames = (requestBody?.tools ?? [])
            .map((tool) => tool.function?.name)
            .filter((name) => typeof name === 'string' && name.length > 0);
        const pass = toolNames.includes(toolName);
        const formattedContext = formatExchangeContext(received)
            .split('\n')
            .map((line) => `  ${line}`)
            .join('\n');
        return {
            pass,
            message: () => pass
                ? `expected request not to contain tool "${toolName}"\n  Tools present: [${toolNames.join(', ')}]\n${formattedContext}`
                : `expected request to contain tool "${toolName}"\n  Tools present: [${toolNames.join(', ')}]\n${formattedContext}`,
        };
    },
    toHaveToolCall(received, toolName) {
        const responseBody = received.response.body;
        const callNames = (responseBody?.choices ?? [])
            .flatMap((choice) => choice.message?.tool_calls ?? [])
            .map((toolCall) => toolCall.function?.name)
            .filter((name) => typeof name === 'string' && name.length > 0);
        const pass = callNames.includes(toolName);
        const formattedContext = formatExchangeContext(received)
            .split('\n')
            .map((line) => `  ${line}`)
            .join('\n');
        return {
            pass,
            message: () => pass
                ? `expected response not to have tool_call "${toolName}"\n  tool_calls present: [${callNames.join(', ')}]\n${formattedContext}`
                : `expected response to have tool_call "${toolName}"\n  tool_calls present: [${callNames.join(', ')}]\n${formattedContext}`,
        };
    },
    toHaveToolResult(received, toolName, contentMatcher) {
        const requestBody = received.request.body;
        const toolMessages = (requestBody?.messages ?? []).filter((message) => message.role === 'tool');
        const matchingToolMessages = toolMessages.filter((message) => message.name === toolName);
        const formattedContext = formatExchangeContext(received)
            .split('\n')
            .map((line) => `  ${line}`)
            .join('\n');
        if (matchingToolMessages.length === 0) {
            const toolMessageList = toolMessages.length === 0
                ? '  (none)'
                : toolMessages
                    .map((message) => {
                    const name = message.name && message.name.length > 0
                        ? message.name
                        : '(unnamed)';
                    const preview = truncate(messageContentToString(message.content), 80);
                    return `  - ${name}: "${preview}"`;
                })
                    .join('\n');
            return {
                pass: false,
                message: () => `expected request to have tool result for "${toolName}", but no tool message with that name found\n\nTool messages in request:\n${toolMessageList}\n\nFull exchange:\n${formattedContext}`,
            };
        }
        if (contentMatcher === undefined) {
            return {
                pass: true,
                message: () => `expected request not to have tool result for "${toolName}"\n\nFull exchange:\n${formattedContext}`,
            };
        }
        const contents = matchingToolMessages.map((message) => messageContentToString(message.content));
        const matchedContent = contents.find((content) => matchesContent(content, contentMatcher));
        const pass = matchedContent !== undefined;
        const actualContent = contents[0] ?? '';
        return {
            pass,
            message: () => pass
                ? `expected tool result for "${toolName}" not to match ${String(contentMatcher)}\n  Actual content: "${matchedContent ?? ''}"\n\nFull exchange:\n${formattedContext}`
                : `expected tool result for "${toolName}" to match ${String(contentMatcher)}\n  Actual content: "${actualContent}"\n\nFull exchange:\n${formattedContext}`,
        };
    },
    async toHaveHealthyProxy(received) {
        const log = await received.proxyLog();
        if (log === null) {
            return {
                pass: true,
                message: () => 'expected proxy not to be healthy, but proxy is not enabled',
            };
        }
        const hasListening = log.includes('Proxy server listening on');
        const hasErrors = log.includes('Error:');
        if (hasErrors) {
            return {
                pass: false,
                message: () => `expected healthy proxy, but proxy log contains errors\n\nProxy log:\n${log}`,
            };
        }
        if (!hasListening) {
            return {
                pass: false,
                message: () => `expected healthy proxy, but proxy log missing listening confirmation\n\nProxy log:\n${log}`,
            };
        }
        return {
            pass: true,
            message: () => `expected proxy not to be healthy\n\nProxy log:\n${log}`,
        };
    },
});
//# sourceMappingURL=matchers.js.map