import * as fs from "fs";
import * as path from "path";
import { collectDiagnostics } from "../vscode-extension/src/diagnostics.js";
import * as vscode from "./__mocks__/vscode.js";
import { KeywordCapitalizationCodeActionProvider } from "../vscode-extension/src/codeActionProvider.js";
import { validateSpecAgainstSchema } from "./specSchemaValidator.js";

import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const Severity = {
    error: vscode.DiagnosticSeverity.Error,
    warning: vscode.DiagnosticSeverity.Warning,
    information: vscode.DiagnosticSeverity.Information
};

function createDocumentFromFile(filePath) {
    const content = fs.readFileSync(filePath, "utf8");
    const lines = content.split(/\r?\n/);

    return {
        lineCount: lines.length,
        lineAt: (index) => ({ text: lines[index] || "" }),
        getText: () => content,
        uri: { fsPath: filePath, toString: () => `file://${filePath}` },
        fileName: filePath,
        languageId: "acurity"
    };
}


function normalizeSeverityValue(severity, sourceText) {
    if (severity === undefined || severity === null) {
        return null;
    }

    const normalized = String(severity).toLowerCase();
    if (normalized !== "error" && normalized !== "warning" && normalized !== "information") {
        throw new Error(`Invalid severity '${severity}' in ${sourceText}. Supported values: error, warning, information.`);
    }

    return normalized;
}

function normalizeExpectation(expectation) {
    if (typeof expectation === "string") {
        return { message: expectation, line: null, id: null, params: null, severity: null };
    }
    if (typeof expectation === "object") {
        const severity = normalizeSeverityValue(expectation.severity, JSON.stringify(expectation));
        if (expectation.id) {
            // ID-based expectation
            return { id: expectation.id, line: expectation.line ?? null, params: expectation.params ?? null, message: null, severity };
        }
        if (expectation.message) {
            // Legacy message-based expectation
            return { message: expectation.message, line: expectation.line ?? null, id: null, params: null, severity };
        }
    }
    throw new Error(`Invalid expectation format: ${JSON.stringify(expectation)}`);
}

function getMessageExpectations(spec) {
    const hasAggregatedShape = ("expected-messages" in spec) || ("unexpected-messages" in spec);
    if (hasAggregatedShape) {
        return {
            expected: (spec["expected-messages"] || []).map((raw) => normalizeExpectation(raw)),
            unexpected: (spec["unexpected-messages"] || []).map((raw) => {
                const normalized = normalizeExpectation(raw);
                // Unexpected checks are file-level by default, so line is ignored.
                return { ...normalized, line: null };
            })
        };
    }

    function withSeverity(raw, severity) {
        if (typeof raw === "string") {
            return { message: raw, severity };
        }
        return { ...raw, severity: raw.severity || severity };
    }

    return {
        expected: [
            ...(spec["expected-errors"] || []).map((raw) => normalizeExpectation(withSeverity(raw, "error"))),
            ...(spec["expected-warnings"] || []).map((raw) => normalizeExpectation(withSeverity(raw, "warning")))
        ],
        unexpected: [
            ...(spec["unexpected-errors"] || []).map((raw) => normalizeExpectation({ ...withSeverity(raw, "error"), line: null })),
            ...(spec["unexpected-warnings"] || []).map((raw) => normalizeExpectation({ ...withSeverity(raw, "warning"), line: null }))
        ]
    };
}

// ---------------------------------------------------------------------------
// v2.0 helpers
// ---------------------------------------------------------------------------

function normalizeV2Assertion(raw) {
    if (typeof raw !== "object" || raw === null) {
        throw new Error(`Invalid v2 assertion: ${JSON.stringify(raw)}`);
    }
    const severity = normalizeSeverityValue(raw.severity, JSON.stringify(raw));
    return {
        id:       raw.id       ?? null,
        message:  raw.message  ?? null,
        line:     raw.line     ?? null,
        severity,
        count:    raw.count    ?? null,
        params:   raw.params   ?? null
    };
}

function getExpectationsV2(spec, modeBlock) {
    const topExists   = (spec["assert-message-exists"]     || []).map(normalizeV2Assertion);
    const topAbsent   = (spec["assert-message-not-exists"] || []).map((r) => ({ ...normalizeV2Assertion(r), line: null }));
    const modeExists  = ((modeBlock && modeBlock["assert-message-exists"])     || []).map(normalizeV2Assertion);
    const modeAbsent  = ((modeBlock && modeBlock["assert-message-not-exists"]) || []).map((r) => ({ ...normalizeV2Assertion(r), line: null }));
    return {
        expected:   [...topExists,  ...modeExists],
        unexpected: [...topAbsent,  ...modeAbsent]
    };
}

/**
 * Count how many diagnostics in the array match the given expectation.
 */
function countMatchingDiagnostics(diagnostics, expectation) {
    return diagnostics.filter((d) => {
        if (expectation.severity && toSeverityName(d.severity) !== expectation.severity) {
            return false;
        }
        if (expectation.id) {
            return matchesDiagnosticById(d, expectation);
        }
        if (expectation.line !== null && (d.range?.start?.line ?? 0) !== expectation.line - 1) {
            return false;
        }
        return matchesMessage(d.message, expectation.message, "substring");
    }).length;
}

/**
 * Core v2 check: apply exists/absent/assert-only-listed rules against a diagnostics array.
 * Appends failure messages to `failures`.
 * `label` is an optional prefix for failures (e.g. "[strict] ") for per-mode runs.
 */
function checkDiagnosticsV2(diagnostics, expected, unexpected, assertOnlyListed, failures, label) {
    const prefix = label ? `${label}: ` : "";

    // assert-message-exists + optional count
    expected.forEach((expectation) => {
        if (expectation.count !== null) {
            const actual = countMatchingDiagnostics(diagnostics, expectation);
            if (actual !== expectation.count) {
                const lineInfo = expectation.line !== null ? ` on line ${expectation.line}` : "";
                failures.push(`${prefix}Expected exactly ${expectation.count} instance(s) of "${expectationToFailureText(expectation)}"${lineInfo} but found ${actual}`);
            }
        } else if (!containsDiagnostic(diagnostics, expectation, "substring")) {
            const lineInfo = expectation.line !== null ? ` on line ${expectation.line}` : "";
            failures.push(`${prefix}Expected message not found: "${expectationToFailureText(expectation)}"${lineInfo}`);
        }
    });

    // assert-message-not-exists
    unexpected.forEach((expectation) => {
        if (containsDiagnostic(diagnostics, expectation, "substring")) {
            failures.push(`${prefix}Unexpected message found: "${expectationToFailureText(expectation)}"`);
        }
    });

    // assert-only-listed
    if (assertOnlyListed) {
        diagnostics.forEach((diagnostic) => {
            const isExpected = expected.some((exp) => expectationMatchesDiagnostic(exp, diagnostic, "substring"));
            if (!isExpected) {
                const id   = diagnostic.data?.messageId || "?";
                const line = (diagnostic.range?.start?.line ?? 0) + 1;
                failures.push(`${prefix}Unlisted diagnostic ${id} on line ${line}: "${diagnostic.message}"`);
            }
        });
    }
}

async function collectDiagnosticsWithProfile(document, profile) {
    vscode.workspace.setConfigOverride("acurity.rules", "profile", profile);
    vscode.workspace.setConfigOverride("acurity.rules", "forceProfile", true);
    try {
        return await collectDiagnostics(document);
    } finally {
        vscode.workspace.clearConfigOverrides();
    }
}

async function runSpecV2(spec, document, baseDir, failures) {
    const assertOnlyListed = spec["assert-only-listed"] === true;

    const testConfig = spec["test-config"] || {};
    Object.entries(testConfig).forEach(([dotKey, value]) => {
        const dotIdx = dotKey.lastIndexOf(".");
        if (dotIdx < 0) {
            console.error(`[test-config] Invalid key "${dotKey}": must be in "section.key" format (no dot found). Entry skipped.`);
            return;
        }
        const section = dotKey.substring(0, dotIdx);
        const key = dotKey.substring(dotIdx + 1);
        vscode.workspace.setConfigOverride(section, key, value);
    });

    try {
        const MODES = ["strict", "default", "legacy"];
        const declaredModes = MODES.filter((m) => (`mode-${m}` in spec));

        if (declaredModes.length === 0) {
            // Single run — no profile forced; honour @mode annotation or defaults.
            const diagnostics = await collectDiagnostics(document);
            const { expected, unexpected } = getExpectationsV2(spec, null);
            checkDiagnosticsV2(diagnostics, expected, unexpected, assertOnlyListed, failures, null);
            testCodeActions(spec, document, diagnostics, baseDir, failures);
            return diagnostics;
        }

        // Per-mode runs.
        let lastDiagnostics = [];
        for (const mode of declaredModes) {
            const modeBlock   = spec[`mode-${mode}`];
            const diagnostics = await collectDiagnosticsWithProfile(document, mode);
            const { expected, unexpected } = getExpectationsV2(spec, modeBlock);
            checkDiagnosticsV2(diagnostics, expected, unexpected, assertOnlyListed, failures, `[${mode}]`);
            lastDiagnostics = diagnostics;
        }
        testCodeActions(spec, document, lastDiagnostics, baseDir, failures);
        return lastDiagnostics;
    } finally {
        if (Object.keys(testConfig).length > 0) {
            vscode.workspace.clearConfigOverrides();
        }
    }
}

function matchesMessage(message, needle, mode) {
    if (mode === "exact") {
        return message === needle;
    }

    return message.includes(needle);
}

function containsMessage(messages, needle, mode) {
    return messages.some((m) => matchesMessage(m, needle, mode));
}

/**
 * Matches a diagnostic against an ID-based expectation.
 * @param {vscode.Diagnostic} diagnostic - The diagnostic to match
 * @param {Object} expectation - Normalized expectation with id, line, params
 * @returns {boolean} - True if diagnostic matches
 */
function matchesDiagnosticById(diagnostic, expectation) {
    const diagData = diagnostic.data;
    if (!diagData || diagData.messageId !== expectation.id) {
        return false;
    }

    // If line specified, check it
    if (expectation.line !== null) {
        if (diagnostic.range.start.line !== expectation.line - 1) {
            return false;
        }
    }

    // If params specified, validate them
    if (expectation.params) {
        for (const [key, value] of Object.entries(expectation.params)) {
            if (diagData.params?.[key] !== value) {
                return false;
            }
        }
    }

    return true;
}

function containsDiagnostic(diagnostics, expectation, mode) {
    // If expectation has an ID, use ID matching (priority over substring)
    if (expectation.id) {
        return diagnostics.some((d) => {
            if (expectation.severity && toSeverityName(d.severity) !== expectation.severity) {
                return false;
            }
            return matchesDiagnosticById(d, expectation);
        });
    }

    // Fall back to legacy substring matching
    return diagnostics.some((diagnostic) => {
        if (expectation.severity && toSeverityName(diagnostic.severity) !== expectation.severity) {
            return false;
        }
        const messageMatches = matchesMessage(diagnostic.message, expectation.message, mode);
        if (!messageMatches) {
            return false;
        }
        // If line is specified, verify it matches (using 0-based index in diagnostic, 1-based in spec)
        if (expectation.line !== null) {
            return diagnostic.range.start.line === expectation.line - 1;
        }
        return true;
    });
}

function toSeverityName(severity) {
    if (severity === Severity.error) {
        return "error";
    }
    if (severity === Severity.warning) {
        return "warning";
    }
    if (severity === Severity.information) {
        return "information";
    }
    return `severity:${severity}`;
}

function toExpectationLabel(expectation) {
    const kind = expectation.severity || "any";
    if (expectation.id) {
        const paramsText = expectation.params ? ` params=${JSON.stringify(expectation.params)}` : "";
        return `[${kind}] id=${expectation.id}${paramsText}`;
    }
    return `[${kind}] ${expectation.message}`;
}

function toDiagnosticLabel(diagnostic) {
    const kind = toSeverityName(diagnostic.severity);
    const id = diagnostic.data?.messageId;
    if (id) {
        return `[${kind}] id=${id} :: ${diagnostic.message}`;
    }
    return `[${kind}] ${diagnostic.message}`;
}

function expectationMatchesDiagnostic(expectation, diagnostic, mode) {
    if (expectation.severity && toSeverityName(diagnostic.severity) !== expectation.severity) {
        return false;
    }
    if (expectation.id) {
        return matchesDiagnosticById(diagnostic, expectation);
    }
    return matchesMessage(diagnostic.message, expectation.message, mode);
}

function padRight(value, width) {
    const text = String(value ?? "");
    if (text.length >= width) {
        return text;
    }
    return `${text}${" ".repeat(width - text.length)}`;
}

function buildDiagnosticLineReport(spec, diagnostics, mode) {
    const expectedRows = new Map();
    const emittedRows = new Map();
    const { expected: expectedMessages, unexpected: disallowedExpectations } = getMessageExpectations(spec);

    expectedMessages.forEach((normalized) => {
        const rowKey = normalized.line === null ? "*" : String(normalized.line);
        const items = expectedRows.get(rowKey) || [];
        items.push({
            ...normalized,
            label: toExpectationLabel(normalized)
        });
        expectedRows.set(rowKey, items);
    });

    diagnostics.forEach((diagnostic) => {
        const line = diagnostic.range?.start?.line;
        const rowKey = Number.isInteger(line) ? String(line + 1) : "*";
        const items = emittedRows.get(rowKey) || [];
        items.push(diagnostic);
        emittedRows.set(rowKey, items);
    });

    const rowKeys = Array.from(new Set([...expectedRows.keys(), ...emittedRows.keys()]));
    if (rowKeys.length === 0) {
        return "";
    }

    rowKeys.sort((a, b) => {
        if (a === "*") {
            return 1;
        }
        if (b === "*") {
            return -1;
        }
        return Number(a) - Number(b);
    });

    const renderedRows = rowKeys.map((rowKey) => {
        const expectedAtRow = expectedRows.get(rowKey) || [];
        const emittedAtRow = emittedRows.get(rowKey) || [];
        const expectedSeverity = Array.from(new Set(expectedAtRow.map((item) => item.severity || "any"))).join(", ");
        const emittedSeverity = Array.from(new Set(emittedAtRow.map((item) => toSeverityName(item.severity)))).join(", ");

        const hasDisallowedEmission = emittedAtRow.some((diagnostic) => {
            return disallowedExpectations.some((unexpected) => {
                // Unexpected checks are global across the file, so line is ignored here.
                const lineAgnosticExpectation = { ...unexpected, line: null };
                return expectationMatchesDiagnostic(lineAgnosticExpectation, diagnostic, mode);
            });
        });

        let status = "-";
        if (rowKey !== "*") {
            if (hasDisallowedEmission) {
                status = "FAIL";
            } else if (expectedAtRow.length === 0) {
                // Informational row: diagnostics were emitted but not explicitly expected/disallowed.
                status = "-";
            } else {
                const allExpectedMatched = expectedAtRow.every((expected) =>
                    emittedAtRow.some((diagnostic) => expectationMatchesDiagnostic(expected, diagnostic, mode))
                );
                status = allExpectedMatched ? "PASS" : "FAIL";
            }
        }

        return {
            row: rowKey,
            expected: expectedAtRow.map((item) => item.label).join(" ; "),
            emitted: emittedAtRow.map((item) => toDiagnosticLabel(item)).join(" ; "),
            expectedSeverity,
            emittedSeverity,
            status
        };
    });

    const rowWidth = Math.max(3, ...renderedRows.map((row) => row.row.length));
    const expectedWidth = Math.max(8, ...renderedRows.map((row) => row.expected.length));
    const emittedWidth = Math.max(7, ...renderedRows.map((row) => row.emitted.length));
    const expectedSeverityWidth = Math.max(17, ...renderedRows.map((row) => row.expectedSeverity.length));
    const emittedSeverityWidth = Math.max(16, ...renderedRows.map((row) => row.emittedSeverity.length));

    const lines = [];
    lines.push("Line Diagnostic Summary (only rows with expected/emitted diagnostics)");
    lines.push(
        `${padRight("ROW", rowWidth)} | ${padRight("EXPECTED", expectedWidth)} | ${padRight("EMITTED", emittedWidth)} | ${padRight("EXPECTED_SEVERITY", expectedSeverityWidth)} | ${padRight("EMITTED_SEVERITY", emittedSeverityWidth)} | STATUS`
    );
    lines.push(
        `${"-".repeat(rowWidth)}-+-${"-".repeat(expectedWidth)}-+-${"-".repeat(emittedWidth)}-+-${"-".repeat(expectedSeverityWidth)}-+-${"-".repeat(emittedSeverityWidth)}-+-------`
    );

    renderedRows.forEach((row) => {
        lines.push(
            `${padRight(row.row, rowWidth)} | ${padRight(row.expected || "", expectedWidth)} | ${padRight(row.emitted || "", emittedWidth)} | ${padRight(row.expectedSeverity || "", expectedSeverityWidth)} | ${padRight(row.emittedSeverity || "", emittedSeverityWidth)} | ${row.status}`
        );
    });

    return lines.join("\n");
}

function expectationToFailureText(expectation) {
    const severityText = expectation.severity ? ` severity=${expectation.severity}` : "";
    if (expectation.id) {
        const paramsText = expectation.params ? ` params=${JSON.stringify(expectation.params)}` : "";
        return `id=${expectation.id}${severityText}${paramsText}`;
    }
    return `${expectation.message}${severityText}`;
}

/**
 * Applies a code action's WorkspaceEdit to a document and returns the modified text.
 */
function applyWorkspaceEdit(document, edit) {
    const lines = [];
    for (let i = 0; i < document.lineCount; i++) {
        lines.push(document.lineAt(i).text);
    }

    // Get edits for this document's URI
    const editsForDoc = edit.edits.get(document.uri) || [];

    // Apply edits in reverse order (end to start) to maintain correct positions
    const sortedEdits = [...editsForDoc].sort((a, b) => {
        if (a.range) {
            const aStart = a.range.start.line * 10000 + a.range.start.character;
            const bStart = b.range ? b.range.start.line * 10000 + b.range.start.character :
                           b.position.line * 10000 + b.position.character;
            return bStart - aStart;
        }
        const aStart = a.position.line * 10000 + a.position.character;
        const bStart = b.position ? b.position.line * 10000 + b.position.character :
                       b.range.start.line * 10000 + b.range.start.character;
        return bStart - aStart;
    });

    sortedEdits.forEach((edit) => {
        if (edit.range) {
            // Replace or delete operation
            const startLine = edit.range.start.line;
            const endLine = edit.range.end.line;
            const startChar = edit.range.start.character;
            const endChar = edit.range.end.character;

            if (startLine === endLine) {
                const line = lines[startLine];
                lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
            } else {
                // Multi-line edit
                const firstPart = lines[startLine].substring(0, startChar);
                const lastPart = lines[endLine].substring(endChar);
                lines.splice(startLine, endLine - startLine + 1, firstPart + edit.newText + lastPart);
            }
        } else if (edit.position) {
            // Insert operation
            const line = lines[edit.position.line];
            lines[edit.position.line] =
                line.substring(0, edit.position.character) +
                edit.newText +
                line.substring(edit.position.character);
        }
    });

    return lines.join('\n');
}

/**
 * Tests code actions if specified in the test spec.
 */
function testCodeActions(spec, document, diagnostics, baseDir, failures) {
    if (!spec["code-action-tests"]) {
        return; // No code action tests for this spec
    }

    const provider = new KeywordCapitalizationCodeActionProvider();

    spec["code-action-tests"].forEach((test, index) => {
        const testNum = index + 1;

        // Find matching diagnostic(s) by ID or message
        const matchingDiags = diagnostics.filter((d) => {
            const lineMatches = test.line ? d.range.start.line === test.line - 1 : true;

            // Support ID-based matching (new architecture)
            if (test.id) {
                const idMatches = d.data?.messageId === test.id;
                return lineMatches && idMatches;
            }

            // Fall back to legacy message substring matching
            if (test["diagnostic-match"]) {
                const messageMatches = d.message.includes(test["diagnostic-match"]);
                return lineMatches && messageMatches;
            }

            return false;
        });

        if (matchingDiags.length === 0) {
            const matchInfo = test.id ? `ID "${test.id}"` : `message "${test["diagnostic-match"]}"`;
            const lineInfo = test.line ? ` on line ${test.line}` : 'any';
            failures.push(`Code action test ${testNum}: No matching diagnostic found for ${matchInfo} on ${lineInfo}`);
            return;
        }

        const diagnostic = matchingDiags[0];

        // Get code actions for this diagnostic
        const range = new vscode.Range(diagnostic.range.start.line, 0, diagnostic.range.start.line, 0);
        const context = { diagnostics: [diagnostic] };
        const actions = provider.provideCodeActions(document, range, context);

        if (!actions || actions.length === 0) {
            failures.push(`Code action test ${testNum}: No code actions available for diagnostic`);
            return;
        }

        // Determine which action to apply (default to 0 for backward compatibility)
        const actionIndex = test["action-index"] !== undefined ? test["action-index"] : 0;

        if (actionIndex < 0 || actionIndex >= actions.length) {
            failures.push(`Code action test ${testNum}: action-index ${actionIndex} out of bounds (${actions.length} action(s) available). Available actions: ${actions.map(a => `"${a.title}"`).join(", ")}`);
            return;
        }

        const action = actions[actionIndex];
        if (!action.edit) {
            failures.push(`Code action test ${testNum}: Code action has no edit`);
            return;
        }

        const fixedContent = applyWorkspaceEdit(document, action.edit);

        // Load expected fixed content
        const expectedFixedPath = path.join(baseDir, test["expected-fixed"]);
        if (!fs.existsSync(expectedFixedPath)) {
            failures.push(`Code action test ${testNum}: Expected fixed file not found: ${test["expected-fixed"]}`);
            return;
        }

        const expectedFixed = fs.readFileSync(expectedFixedPath, "utf8");

        // Normalize line endings for comparison
        const normalizedFixed = fixedContent.replace(/\r\n/g, '\n').trim();
        const normalizedExpected = expectedFixed.replace(/\r\n/g, '\n').trim();

        if (normalizedFixed !== normalizedExpected) {
            failures.push(`Code action test ${testNum}: Fixed content doesn't match expected.\nGot:\n${normalizedFixed}\n\nExpected:\n${normalizedExpected}`);
        }
    });
}

/**
 * Recursively finds all test spec files in a directory and subdirectories.
 */
function findAllTestSpecs(dir) {
    const specs = [];

    function scan(currentDir) {
        const entries = fs.readdirSync(currentDir, { withFileTypes: true });

        for (const entry of entries) {
            const fullPath = path.join(currentDir, entry.name);

            if (entry.isDirectory()) {
                scan(fullPath);
            } else if (entry.name.endsWith('.test-spec.json')) {
                specs.push({
                    specPath: fullPath,
                    baseDir: currentDir
                });
            }
        }
    }

    scan(dir);
    return specs.sort((a, b) => a.specPath.localeCompare(b.specPath));
}

async function runSingleSpec(specPath, baseDir) {
    const spec = JSON.parse(fs.readFileSync(specPath, "utf8"));
    validateSpecAgainstSchema(spec, specPath);

    const entryPath = path.join(baseDir, spec["test-entry"]);
    if (!fs.existsSync(entryPath)) {
        throw new Error(`${path.basename(specPath)} references missing entry file: ${spec["test-entry"]}`);
    }

    const document = createDocumentFromFile(entryPath);
    const failures = [];

    let diagnostics;

    if (spec["schema-version"] === "2.0") {
        diagnostics = await runSpecV2(spec, document, baseDir, failures);
    } else {
        // v1.0 path — unchanged
        const testConfig = spec["test-config"] || {};
        Object.entries(testConfig).forEach(([dotKey, value]) => {
            const dotIdx = dotKey.lastIndexOf(".");
            const section = dotKey.substring(0, dotIdx);
            const key = dotKey.substring(dotIdx + 1);
            vscode.workspace.setConfigOverride(section, key, value);
        });

        try {
            diagnostics = await collectDiagnostics(document);
        } finally {
            vscode.workspace.clearConfigOverrides();
        }

        const matchMode = spec["diagnostic-match-mode"] || "substring";
        const { expected: expectedMessages, unexpected: unexpectedMessages } = getMessageExpectations(spec);

        expectedMessages.forEach((expectation) => {
            if (!containsDiagnostic(diagnostics, expectation, matchMode)) {
                const lineInfo = expectation.line !== null ? ` on line ${expectation.line}` : "";
                failures.push(`Expected message not found: "${expectationToFailureText(expectation)}"${lineInfo}`);
            }
        });

        unexpectedMessages.forEach((expectation) => {
            if (containsDiagnostic(diagnostics, expectation, matchMode)) {
                failures.push(`Unexpected message found: "${expectationToFailureText(expectation)}"`);
            }
        });

        if (matchMode === "exact") {
            diagnostics.forEach((diagnostic) => {
                const isExpected = expectedMessages.some((exp) => expectationMatchesDiagnostic(exp, diagnostic, matchMode));
                if (!isExpected) {
                    const id = diagnostic.data?.messageId || "?";
                    const line = (diagnostic.range?.start?.line ?? 0) + 1;
                    failures.push(`Exact mode: unlisted diagnostic ${id} on line ${line}: "${diagnostic.message}"`);
                }
            });
        }

        testCodeActions(spec, document, diagnostics, baseDir, failures);

        if (failures.length > 0) {
            const lineReport = buildDiagnosticLineReport(spec, diagnostics, matchMode);
            if (lineReport) {
                failures.push(lineReport);
            }
        }
    }

    return {
        id: spec["test-id"],
        name: spec["test-name"],
        specFile: path.basename(specPath),
        entryFile: spec["test-entry"],
        pass: failures.length === 0,
        failures,
        diagnosticsCount: diagnostics.length
    };
}

async function runAllSyntaxSpecs() {
    const baseDir = path.join(__dirname, "syntax-tests");
    const testSpecs = findAllTestSpecs(baseDir);

    const results = [];
    for (const { specPath, baseDir: specBaseDir } of testSpecs) {
        const result = await runSingleSpec(specPath, specBaseDir);
        const relativeSpecPath = path.relative(baseDir, specPath);
        const suiteDir = path.dirname(relativeSpecPath);

        results.push({
            ...result,
            suite: suiteDir === "." ? "root" : suiteDir.replace(/\\/g, "/"),
            specPath: relativeSpecPath.replace(/\\/g, "/")
        });
    }

    const passed = results.filter((r) => r.pass).length;

    return {
        total: results.length,
        passed,
        failed: results.length - passed,
        results
    };
}

export {
    runAllSyntaxSpecs,
    runSingleSpec
};
