import * as vscode from "vscode";
import type * as lc from "vscode-languageclient/node";
import * as ra from "./lsp_ext";

import type { Ctx } from "./ctx";
import { startDebugSession } from "./debug";

export const prepareTestExplorer = (
    ctx: Ctx,
    testController: vscode.TestController,
    client: lc.LanguageClient,
) => {
    let currentTestRun: vscode.TestRun | undefined;
    let idToTestMap: Map<string, vscode.TestItem> = new Map();
    const fileToTestMap: Map<string, vscode.TestItem[]> = new Map();
    const idToRunnableMap: Map<string, ra.Runnable> = new Map();

    testController.createRunProfile(
        "Run Tests",
        vscode.TestRunProfileKind.Run,
        async (request: vscode.TestRunRequest, cancelToken: vscode.CancellationToken) => {
            if (currentTestRun) {
                await client.sendNotification(ra.abortRunTest);
                while (currentTestRun) {
                    await new Promise((resolve) => setTimeout(resolve, 1));
                }
            }

            currentTestRun = testController.createTestRun(request);
            cancelToken.onCancellationRequested(async () => {
                await client.sendNotification(ra.abortRunTest);
            });
            const include = request.include?.map((x) => x.id);
            const exclude = request.exclude?.map((x) => x.id);
            await client.sendRequest(ra.runTest, { include, exclude });
        },
        true,
        undefined,
        false,
    );

    testController.createRunProfile(
        "Debug Tests",
        vscode.TestRunProfileKind.Debug,
        async (request: vscode.TestRunRequest) => {
            if (request.include?.length !== 1 || request.exclude?.length !== 0) {
                await vscode.window.showErrorMessage("You can debug only one test at a time");
                return;
            }
            const id = request.include[0]!.id;
            const runnable = idToRunnableMap.get(id);
            if (!runnable) {
                await vscode.window.showErrorMessage("You can debug only one test at a time");
                return;
            }
            await startDebugSession(ctx, runnable);
        },
        true,
        undefined,
        false,
    );

    const deleteTest = (item: vscode.TestItem, parentList: vscode.TestItemCollection) => {
        parentList.delete(item.id);
        idToTestMap.delete(item.id);
        idToRunnableMap.delete(item.id);
        if (item.uri) {
            fileToTestMap.set(
                item.uri.toString(),
                fileToTestMap.get(item.uri.toString())!.filter((t) => t.id !== item.id),
            );
        }
    };

    const addTest = (item: ra.TestItem) => {
        const parentList = item.parent
            ? idToTestMap.get(item.parent)!.children
            : testController.items;
        const oldTest = parentList.get(item.id);
        const uri = item.textDocument?.uri ? vscode.Uri.parse(item.textDocument?.uri) : undefined;
        const range =
            item.range &&
            new vscode.Range(
                new vscode.Position(item.range.start.line, item.range.start.character),
                new vscode.Position(item.range.end.line, item.range.end.character),
            );
        if (oldTest) {
            if (oldTest.uri?.toString() === uri?.toString()) {
                oldTest.range = range;
                return;
            }
            deleteTest(oldTest, parentList);
        }
        const iconToVscodeMap = {
            package: "package",
            module: "symbol-module",
            test: "beaker",
        };
        const test = testController.createTestItem(
            item.id,
            `$(${iconToVscodeMap[item.kind]}) ${item.label}`,
            uri,
        );
        test.range = range;
        test.canResolveChildren = item.canResolveChildren;
        idToTestMap.set(item.id, test);
        if (uri) {
            if (!fileToTestMap.has(uri.toString())) {
                fileToTestMap.set(uri.toString(), []);
            }
            fileToTestMap.get(uri.toString())!.push(test);
        }
        if (item.runnable) {
            idToRunnableMap.set(item.id, item.runnable);
        }
        parentList.add(test);
    };

    const addTestGroup = (testsAndScope: ra.DiscoverTestResults) => {
        const { tests, scope, scopeFile } = testsAndScope;
        const testSet: Set<string> = new Set();
        for (const test of tests) {
            addTest(test);
            testSet.add(test.id);
        }
        // FIXME(hack_recover_crate_name): We eagerly resolve every test if we got a lazy top level response (detected
        // by checking that `scope` is empty). This is not a good thing and wastes cpu and memory unnecessarily, so we
        // should remove it.
        if (!scope && !scopeFile) {
            for (const test of tests) {
                void testController.resolveHandler!(idToTestMap.get(test.id));
            }
        }
        if (scope) {
            const recursivelyRemove = (tests: vscode.TestItemCollection) => {
                for (const [_, test] of tests) {
                    if (!testSet.has(test.id)) {
                        deleteTest(test, tests);
                    } else {
                        recursivelyRemove(test.children);
                    }
                }
            };
            for (const root of scope) {
                recursivelyRemove(idToTestMap.get(root)!.children);
            }
        }
        if (scopeFile) {
            const removeByFile = (file: vscode.Uri) => {
                const testsToBeRemoved = (fileToTestMap.get(file.toString()) || []).filter(
                    (t) => !testSet.has(t.id),
                );
                for (const test of testsToBeRemoved) {
                    const parentList = test.parent?.children || testController.items;
                    deleteTest(test, parentList);
                }
            };
            for (const file of scopeFile) {
                removeByFile(vscode.Uri.parse(file.uri));
            }
        }
    };

    ctx.pushClientCleanup(
        client.onNotification(ra.discoveredTests, (results) => {
            addTestGroup(results);
        }),
    );

    ctx.pushClientCleanup(
        client.onNotification(ra.endRunTest, () => {
            currentTestRun!.end();
            currentTestRun = undefined;
        }),
    );

    ctx.pushClientCleanup(
        client.onNotification(ra.appendOutputToRunTest, (output) => {
            currentTestRun!.appendOutput(`${output}\r\n`);
        }),
    );

    ctx.pushClientCleanup(
        client.onNotification(ra.changeTestState, (results) => {
            const test = idToTestMap.get(results.testId)!;
            if (results.state.tag === "failed") {
                currentTestRun!.failed(test, new vscode.TestMessage(results.state.message));
            } else if (results.state.tag === "passed") {
                currentTestRun!.passed(test);
            } else if (results.state.tag === "started") {
                currentTestRun!.started(test);
            } else if (results.state.tag === "skipped") {
                currentTestRun!.skipped(test);
            } else if (results.state.tag === "enqueued") {
                currentTestRun!.enqueued(test);
            }
        }),
    );

    testController.resolveHandler = async (item) => {
        const results = await client.sendRequest(ra.discoverTest, { testId: item?.id });
        addTestGroup(results);
    };

    testController.refreshHandler = async () => {
        testController.items.forEach((t) => {
            testController.items.delete(t.id);
        });
        idToTestMap = new Map();
        await testController.resolveHandler!(undefined);
    };
};
