import { expect, test } from "bun:test";
import { fileURLToPath } from "node:url";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
  completionItems,
  definitionLocation,
  documentDiagnostics,
  documentSymbols,
  formatDocument,
  positionAt,
  symbolLocations,
  virtualTypeScriptDocument,
  workspaceCompletionItems,
  clearWorkspaceIndexCache,
  extractComponentRefactor,
  htmlToWrn,
} from "../src/index.ts";

const document = {
  uri: "file:///Counter.wrn",
  text: `component Counter {\nstate count = 0\nview { <button @click='count++'>{count}</button> }\n}`,
};

test("provides editor-neutral language features", () => {
  expect(documentDiagnostics(document)).toEqual([]);
  expect(completionItems().some((item) => item.label === "state")).toBe(true);
  expect(documentSymbols(document).map((item) => item.name)).toContain("count");
  const reference = positionAt(document.text, document.text.lastIndexOf("count"));
  expect(definitionLocation(document, reference)).not.toBeNull();
  expect(symbolLocations(document, reference)).toHaveLength(3);
  expect(formatDocument(document)).toBeArray();
});

test("indexes workspace components, contracts, routes, translations, schemas and classes", () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-lsp-workspace-"));
  try {
    for (const directory of ["components", "pages", "locales", "db"])
      mkdirSync(join(root, "app", directory), { recursive: true });
    mkdirSync(join(root, "app", "pages", "users"), { recursive: true });
    writeFileSync(
      join(root, "app", "components", "Card.wrn"),
      `component Card { props { title: string } outputs { close() } view { <slot name="body" /> } }`,
    );
    writeFileSync(
      join(root, "app", "pages", "users", "[id].wrn"),
      `page User { view { <p class="text-red-500">User</p> } }`,
      { flag: "w" },
    );
    writeFileSync(
      join(root, "app", "locales", "en.json"),
      JSON.stringify({ user: { title: "User" } }),
    );
    writeFileSync(join(root, "app", "db", "schema.ts"), `export const UserSchema = {};`);
    const labels = workspaceCompletionItems(root).map((item) => item.label);
    for (const label of [
      "Card",
      "title",
      "@close",
      "slot:body",
      "/users/:id",
      "user.title",
      "UserSchema",
      "text-red-500",
    ])
      expect(labels).toContain(label);
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("workspace indexing is cached and skips dependency and generated trees", () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-lsp-bounded-"));
  try {
    mkdirSync(join(root, "app", "pages"), { recursive: true });
    mkdirSync(join(root, "app", "node_modules", "large-package"), { recursive: true });
    mkdirSync(join(root, "app", ".wrnexus"), { recursive: true });
    writeFileSync(join(root, "app", "pages", "index.wrn"), `page Home { view { <p>Home</p> } }`);
    writeFileSync(
      join(root, "app", "node_modules", "large-package", "secret.ts"),
      `export const DependencySchema = {};`,
    );
    writeFileSync(join(root, "app", ".wrnexus", "generated.ts"), `export const CacheSchema = {};`);

    const first = workspaceCompletionItems(root);
    const second = workspaceCompletionItems(root);
    expect(second).toBe(first);
    expect(first.map((item) => item.label)).not.toContain("DependencySchema");
    expect(first.map((item) => item.label)).not.toContain("CacheSchema");

    writeFileSync(join(root, "app", "pages", "later.wrn"), `page Later { view { <p>Later</p> } }`);
    expect(workspaceCompletionItems(root).map((item) => item.label)).not.toContain("/later");
    clearWorkspaceIndexCache(root);
    expect(workspaceCompletionItems(root).map((item) => item.label)).toContain("/later");
  } finally {
    clearWorkspaceIndexCache(root);
    rmSync(root, { recursive: true, force: true });
  }
});

test("provides safe component extraction and HTML conversion refactors", () => {
  const doc = {
    uri: "file:///project/app/pages/index.wrn",
    version: 2,
    text: "<section>Hello</section>",
  };
  const range = { start: { line: 0, character: 0 }, end: { line: 0, character: doc.text.length } };
  const edit = extractComponentRefactor(doc, range, "Greeting");
  expect(edit.documentChanges).toHaveLength(3);
  expect(JSON.stringify(edit)).toContain("<Greeting />");
  expect(htmlToWrn(`<button onclick="save()">Save</button>`, "Imported")).toContain(
    `@click="save()"`,
  );
});

test("provides a mapped TypeScript virtual document and expression type diagnostics", () => {
  const typed = {
    uri: "file:///Typed.wrn",
    text: `component Typed {\nprops { count: number }\nstate label: string = count\nview { <p>{label}</p> }\n}`,
  };
  const virtual = virtualTypeScriptDocument(typed);
  expect(virtual.languageId).toBe("typescript");
  expect(virtual.uri).toEndWith(".wrn.ts");
  expect(virtual.text).toContain("declare const count: Readonly<number>");
  expect(virtual.mappings.length).toBeGreaterThan(0);
  expect(documentDiagnostics(typed).some((item) => String(item.code).startsWith("WRN-TYPE-"))).toBe(
    true,
  );
});

test("reports compiler and accessibility diagnostics", () => {
  const diagnostics = documentDiagnostics({
    uri: "file:///bad.wrn",
    text: `page Bad {\n  view {\n    <img src='/x.png' />\n  }\n}`,
  });
  expect(diagnostics.some((item) => String(item.code).startsWith("WRN-A11Y"))).toBe(true);
});

test("skips compiler graph creation for oversized WRN documents", () => {
  const diagnostics = documentDiagnostics({
    uri: "file:///large.wrn",
    text: `page Large { view { <p>${"x".repeat(1_048_576)}</p> } }`,
  });
  expect(diagnostics).toHaveLength(1);
  expect(diagnostics[0]?.code).toBe("WRN-LSP-FILE-SIZE");
});

function packet(value: unknown): Uint8Array {
  const body = JSON.stringify(value);
  return new TextEncoder().encode(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
}

test("serves initialize over standard LSP stdio framing", async () => {
  const process = Bun.spawn(
    ["bun", "run", fileURLToPath(new URL("../src/server.ts", import.meta.url))],
    {
      stdin: "pipe",
      stdout: "pipe",
      stderr: "pipe",
    },
  );
  process.stdin.write(packet({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }));
  await process.stdin.flush();
  const reader = process.stdout.getReader();
  let output = "";
  while (!output.includes('"id":1')) {
    const chunk = await reader.read();
    if (chunk.done) break;
    output += new TextDecoder().decode(chunk.value);
  }
  expect(output).toContain("WRNexus Language Server");
  expect(output).toContain("documentFormattingProvider");
  expect(output).toContain("wrnexusVirtualTypeScript");
  process.kill();
  await process.exited;
});

test("coalesces rapid document changes into one pending diagnostic analysis", async () => {
  const process = Bun.spawn(
    ["bun", "run", fileURLToPath(new URL("../src/server.ts", import.meta.url))],
    { stdin: "pipe", stdout: "pipe", stderr: "pipe" },
  );
  const uri = "file:///rapid.wrn";
  process.stdin.write(
    packet({
      jsonrpc: "2.0",
      method: "textDocument/didOpen",
      params: { textDocument: { uri, version: 1, text: `page Rapid { view { <p>1</p> } }` } },
    }),
  );
  for (let version = 2; version <= 50; version++) {
    process.stdin.write(
      packet({
        jsonrpc: "2.0",
        method: "textDocument/didChange",
        params: {
          textDocument: { uri, version },
          contentChanges: [{ text: `page Rapid { view { <p>${version}</p> } }` }],
        },
      }),
    );
  }
  process.stdin.write(
    packet({ jsonrpc: "2.0", id: 99, method: "wrnexus/serverStatus", params: {} }),
  );
  await process.stdin.flush();
  const reader = process.stdout.getReader();
  let output = "";
  while (!output.includes('"id":99')) {
    const chunk = await reader.read();
    if (chunk.done) break;
    output += new TextDecoder().decode(chunk.value);
  }
  expect(output).toContain('"pendingDiagnostics":1');
  expect(output).toContain('"openDocuments":1');
  process.kill();
  await process.exited;
});
