import fs from "fs";
import os from "os";
import path from "path";
import { afterEach, describe, expect, it } from "vitest";
import { buildCoverageGraph, buildServiceCoverageContext } from "./index.mjs";

const cleanups = [];

afterEach(() => {
  while (cleanups.length > 0) {
    cleanups.pop()();
  }
});

describe("coverage graph builder", () => {
  it("builds a Next page -> surface -> action -> request -> API route -> backend/data capability graph", () => {
    const productDir = createProduct();
    writeFile(
      productDir,
      "testkit.setup.ts",
      `
        import { defineTestkitSetup, nextService } from "@elench/testkit/setup";

        export default defineTestkitSetup({
          services: {
            web: nextService({
              cwd: ".",
              start: "node server.js",
              baseUrl: "http://127.0.0.1:3000",
              readyUrl: "http://127.0.0.1:3000"
            })
          }
        });
      `
    );
    writeFile(
      productDir,
      "src/app/campaigns/page.tsx",
      `
        "use client";
        import { getJson, postJson } from "@/client/http/http";

        async function refreshCampaigns() {
          await getJson("/api/campaigns");
        }

        async function createCampaign() {
          await postJson("/api/campaigns");
        }

        export default function CampaignsPage() {
          return (
            <main>
              <button data-testid="campaign-refresh-button" onClick={() => refreshCampaigns()}>
                Refresh campaigns
              </button>
              <button data-testid="campaign-create-button" onClick={createCampaign}>
                Create campaign
              </button>
            </main>
          );
        }
      `
    );
    writeFile(
      productDir,
      "src/client/http/http.ts",
      `export function getJson() { return null; }\nexport function postJson() { return null; }`
    );
    writeFile(
      productDir,
      "src/app/api/campaigns/route.ts",
      `
        import { listCampaigns, createCampaign } from "@/backend/server/campaigns";

        export async function GET() {
          return listCampaigns();
        }

        export async function POST() {
          return createCampaign();
        }
      `
    );
    writeFile(
      productDir,
      "src/backend/server/campaigns/index.ts",
      `
        import { insertCampaignRow, selectCampaignRows } from "@/backend/data/campaigns";

        export async function listCampaigns() {
          return selectCampaignRows();
        }

        export async function createCampaign() {
          return insertCampaignRow();
        }
      `
    );
    writeFile(
      productDir,
      "src/backend/data/campaigns/index.ts",
      `export async function selectCampaignRows() {}\nexport async function insertCampaignRow() {}`
    );

    const context = buildServiceCoverageContext(productDir, "web", {
      local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
    });

    expect(context.pageByRoute.get("/campaigns").node).toMatchObject({
      kind: "page_view",
      route: "/campaigns",
      filePath: "src/app/campaigns/page.tsx",
    });
    expect(context.pageByRoute.get("/campaigns").surfacesByTargetValue.get("campaign-refresh-button")).toMatchObject({
      kind: "ui_surface",
      label: "Refresh campaigns",
    });
    expect(context.apiRouteByKey.get("GET:/api/campaigns").node).toMatchObject({
      kind: "api_route",
      route: "/campaigns",
      method: "GET",
      path: "/api/campaigns",
      filePath: "src/app/api/campaigns/route.ts",
    });
    expect(context.graph.edges).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          kind: "contains",
          from: "page_view:web:/campaigns",
          to: "ui_surface:web:/campaigns:campaign-refresh-button",
        }),
        expect.objectContaining({
          kind: "triggers",
          from: "ui_surface:web:/campaigns:campaign-refresh-button",
          to: expect.stringContaining("ui_action:web:/campaigns"),
        }),
        expect.objectContaining({
          kind: "requests",
          from: expect.stringContaining("ui_action:web:/campaigns"),
        }),
        expect.objectContaining({
          kind: "handles",
          to: "api_route:web:GET:/api/campaigns",
        }),
        expect.objectContaining({
          kind: "delegates_to",
          from: "api_route:web:GET:/api/campaigns",
        }),
        expect.objectContaining({
          kind: "delegates_to",
          from: "server_capability:src/backend/server/campaigns#createCampaign",
          to: "data_capability:src/backend/data/campaigns#insertCampaignRow",
        }),
      ])
    );
  });

  it("discovers server action links from pages", () => {
    const productDir = createProduct();
    writeFile(
      productDir,
      "src/app/settings/page.tsx",
      `
        import { saveSettings } from "./actions";

        export default function SettingsPage() {
          return (
            <form action={saveSettings}>
              <button data-testid="settings-save-button" type="submit">
                Save settings
              </button>
            </form>
          );
        }
      `
    );
    writeFile(
      productDir,
      "src/app/settings/actions.ts",
      `
        "use server";
        import { updateSettings } from "@/backend/server/settings";

        export async function saveSettings() {
          return updateSettings();
        }
      `
    );
    writeFile(productDir, "src/backend/server/settings.ts", `export async function updateSettings() {}`);

    const context = buildServiceCoverageContext(productDir, "web", {
      local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
    });

    expect(context.serverActionByExportKey.get("src/app/settings/actions.ts#saveSettings").node).toMatchObject({
      kind: "server_action",
      label: "saveSettings",
    });
    expect(context.graph.edges).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          kind: "triggers",
          from: expect.stringContaining("ui_surface:web:/settings"),
          to: "ui_action:web:/settings:saveSettings",
        }),
        expect.objectContaining({
          kind: "triggers",
          from: "ui_action:web:/settings:saveSettings",
          to: "server_action:web:src/app/settings/actions.ts#saveSettings",
        }),
        expect.objectContaining({
          kind: "delegates_to",
          from: "server_action:web:src/app/settings/actions.ts#saveSettings",
        }),
      ])
    );
  });

  it("attaches convention-based test evidence to page, surface, route, and data nodes", () => {
    const productDir = createProduct();
    writeFile(
      productDir,
      "src/app/campaigns/page.tsx",
      `
        "use client";
        import { getJson } from "@/client/http/http";

        async function loadCampaigns() {
          await getJson("/api/campaigns");
        }

        export default function CampaignsPage() {
          return <button data-testid="campaign-save-button" onClick={loadCampaigns}>Save campaign</button>;
        }
      `
    );
    writeFile(productDir, "src/client/http/http.ts", `export async function getJson() { return null; }`);
    writeFile(
      productDir,
      "src/app/api/campaigns/route.ts",
      `
        import { listCampaigns } from "@/backend/server/campaigns";

        export async function GET() {
          return listCampaigns();
        }
      `
    );
    writeFile(
      productDir,
      "src/backend/server/campaigns/index.ts",
      `
        import { saveCampaignRow } from "@/backend/data/campaigns";

        export async function listCampaigns() {
          return saveCampaignRow();
        }
      `
    );
    writeFile(productDir, "src/backend/data/campaigns/index.ts", `export async function saveCampaignRow() {}`);
    writeFile(
      productDir,
      "src/app/campaigns/__testkit__/campaigns.pw.testkit.ts",
      `
        import { expect, test } from "@playwright/test";

        test("campaigns route", async ({ page }) => {
          await page.goto("/campaigns");
          await expect(page.getByTestId("campaign-save-button")).toBeVisible();
        });
      `
    );
    writeFile(
      productDir,
      "src/app/api/campaigns/__testkit__/campaigns.int.testkit.ts",
      `
        import { defineHttpSuite } from "@elench/testkit";

        const suite = defineHttpSuite(({ rawReq }) => {
          rawReq("GET", "/api/campaigns");
        });

        export default suite;
      `
    );
    writeFile(
      productDir,
      "src/backend/data/campaigns/__testkit__/campaigns.dal.testkit.ts",
      `
        import { defineDalSuite } from "@elench/testkit";

        const suite = defineDalSuite(({ db }) => {
          db.query("SELECT 1 AS value");
        });

        export default suite;
      `
    );

    const graph = buildCoverageGraph({
      productDir,
      services: {
        web: {
          local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
        },
      },
      discoveryFiles: [
        {
          serviceName: "web",
          type: "e2e",
          framework: "playwright",
          suiteName: "campaigns",
          filePath: "src/app/campaigns/__testkit__/campaigns.pw.testkit.ts",
        },
        {
          serviceName: "web",
          type: "integration",
          framework: "k6",
          suiteName: "campaigns",
          filePath: "src/app/api/campaigns/__testkit__/campaigns.int.testkit.ts",
        },
        {
          serviceName: "web",
          type: "dal",
          framework: "k6",
          suiteName: "campaigns",
          filePath: "src/backend/data/campaigns/__testkit__/campaigns.dal.testkit.ts",
        },
      ],
    });

    expect(graph.evidence).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          testFilePath: "src/app/campaigns/__testkit__/campaigns.pw.testkit.ts",
          coveredNodeIds: expect.arrayContaining([
            "page_view:web:/campaigns",
            "ui_surface:web:/campaigns:campaign-save-button",
          ]),
          details: expect.objectContaining({
            route: "/campaigns",
            targets: [
              expect.objectContaining({
                kind: "testId",
                value: "campaign-save-button",
              }),
            ],
          }),
        }),
        expect.objectContaining({
          testFilePath: "src/app/api/campaigns/__testkit__/campaigns.int.testkit.ts",
          coveredNodeIds: ["api_route:web:GET:/api/campaigns"],
        }),
        expect.objectContaining({
          testFilePath: "src/backend/data/campaigns/__testkit__/campaigns.dal.testkit.ts",
          coveredNodeIds: expect.arrayContaining([
            expect.stringContaining("data_capability:src/backend/data/campaigns"),
          ]),
        }),
      ])
    );
  });

  it("normalizes dynamic routes", () => {
    const productDir = createProduct();
    writeFile(productDir, "src/app/projects/[projectId]/page.tsx", `export default function ProjectPage() { return null; }`);
    writeFile(productDir, "src/app/api/projects/[projectId]/route.ts", `export async function GET() { return Response.json({ ok: true }); }`);

    const context = buildServiceCoverageContext(productDir, "web", {
      local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
    });

    expect(context.pageByRoute.get("/projects/[projectId]").node).toBeTruthy();
    expect(context.apiRouteByKey.get("GET:/api/projects/[projectId]").node).toBeTruthy();
  });

  it("resolves root-level PW test via page.goto() content extraction", () => {
    const productDir = createProduct();
    writeFile(
      productDir,
      "src/app/dashboard/page.tsx",
      `export default function DashboardPage() { return <button data-testid="dash-header">Dashboard</button>; }`
    );
    writeFile(
      productDir,
      "__testkit__/dashboard.pw.testkit.ts",
      `
        import { test, expect } from "@playwright/test";
        test("dashboard loads", async ({ page }) => {
          await page.goto("/dashboard");
          await expect(page.getByTestId("dash-header")).toBeVisible();
        });
      `
    );

    const graph = buildCoverageGraph({
      productDir,
      services: {
        web: {
          local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
        },
      },
      discoveryFiles: [
        {
          serviceName: "web",
          type: "e2e",
          framework: "playwright",
          suiteName: "dashboard",
          filePath: "__testkit__/dashboard.pw.testkit.ts",
        },
      ],
    });

    const evidence = graph.evidence.find((e) => e.testFilePath === "__testkit__/dashboard.pw.testkit.ts");
    expect(evidence.coveredNodeIds).toEqual(
      expect.arrayContaining([
        "page_view:web:/dashboard",
        "ui_surface:web:/dashboard:dash-header",
      ])
    );
  });

  it("resolves root-level e2e test via rawReq content extraction", () => {
    const productDir = createProduct();
    writeFile(
      productDir,
      "src/app/api/projects/route.ts",
      `export async function GET() { return Response.json({ ok: true }); }`
    );
    writeFile(
      productDir,
      "__testkit__/projects.e2e.testkit.ts",
      `
        import { defineHttpSuite } from "@elench/testkit";
        export default defineHttpSuite(({ rawReq }) => {
          rawReq("GET", "/api/projects");
        });
      `
    );

    const graph = buildCoverageGraph({
      productDir,
      services: {
        web: {
          local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
        },
      },
      discoveryFiles: [
        {
          serviceName: "web",
          type: "e2e",
          framework: "k6",
          suiteName: "projects",
          filePath: "__testkit__/projects.e2e.testkit.ts",
        },
      ],
    });

    const evidence = graph.evidence.find((e) => e.testFilePath === "__testkit__/projects.e2e.testkit.ts");
    expect(evidence.coveredNodeIds).toEqual(["api_route:web:GET:/api/projects"]);
  });

  it("resolves scenario test via rawReq content extraction", () => {
    const productDir = createProduct();
    writeFile(
      productDir,
      "src/app/api/campaigns/route.ts",
      `export async function POST() { return Response.json({ ok: true }); }`
    );
    writeFile(
      productDir,
      "__testkit__/campaign-flow.scenario.testkit.ts",
      `
        import { defineHttpSuite } from "@elench/testkit";
        export default defineHttpSuite(({ rawReq }) => {
          rawReq("POST", "/api/campaigns");
        });
      `
    );

    const graph = buildCoverageGraph({
      productDir,
      services: {
        web: {
          local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
        },
      },
      discoveryFiles: [
        {
          serviceName: "web",
          type: "scenario",
          framework: "k6",
          suiteName: "campaign-flow",
          filePath: "__testkit__/campaign-flow.scenario.testkit.ts",
        },
      ],
    });

    const evidence = graph.evidence.find((e) => e.testFilePath === "__testkit__/campaign-flow.scenario.testkit.ts");
    expect(evidence.coveredNodeIds).toEqual(["api_route:web:POST:/api/campaigns"]);
  });

  it("builds route-tree coverage from ancestor layouts, provider effects, local handlers, and dynamic request templates", () => {
    const productDir = createProduct();
    writeFile(
      productDir,
      "src/app/(dashboard)/layout.tsx",
      `
        import { ProjectProvider } from "@/components/project-context";

        export default function DashboardLayout({ children }) {
          return <ProjectProvider>{children}</ProjectProvider>;
        }
      `
    );
    writeFile(
      productDir,
      "src/components/project-context.tsx",
      `
        "use client";
        import { useEffect } from "react";

        export function ProjectProvider({ children }) {
          const fetchProjects = async () => {
            await fetch("/api/projects");
          };

          useEffect(() => {
            fetchProjects();
          }, []);

          return <section>{children}</section>;
        }
      `
    );
    writeFile(
      productDir,
      "src/app/(dashboard)/projects/page.tsx",
      `
        "use client";
        import { useState } from "react";

        export default function ProjectsPage() {
          const [name, setName] = useState("");

          const handleCreate = async (event) => {
            event.preventDefault();
            await fetch("/api/projects", { method: "POST" });
            setName("");
          };

          return (
            <form onSubmit={handleCreate}>
              <input value={name} onChange={(event) => setName(event.target.value)} />
              <button data-testid="project-create-button" type="submit">Create Project</button>
            </form>
          );
        }
      `
    );
    writeFile(
      productDir,
      "src/app/(dashboard)/events/page.tsx",
      `
        "use client";
        import { useEffect } from "react";

        export default function EventsPage() {
          const activeProject = { id: "project-1" };

          useEffect(() => {
            fetch(\`/api/projects/\${activeProject.id}/events?limit=100\`);
          }, [activeProject.id]);

          return <div>Events</div>;
        }
      `
    );
    writeFile(
      productDir,
      "src/app/api/projects/route.ts",
      `
        export async function GET() { return Response.json([]); }
        export async function POST() { return Response.json({}, { status: 201 }); }
      `
    );
    writeFile(
      productDir,
      "src/app/api/projects/[projectId]/events/route.ts",
      `
        export async function GET() { return Response.json([]); }
      `
    );

    const context = buildServiceCoverageContext(productDir, "web", {
      local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
    });

    expect(context.graph.edges).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          kind: "requests",
          from: "page_view:web:/projects",
          to: expect.stringContaining("client_request:web:src/components/project-context.tsx:page:/projects"),
        }),
        expect.objectContaining({
          kind: "handles",
          from: expect.stringContaining("client_request:web:src/components/project-context.tsx:page:/projects"),
          to: "api_route:web:GET:/api/projects",
        }),
        expect.objectContaining({
          kind: "triggers",
          from: expect.stringContaining("ui_surface:web:/projects"),
          to: "ui_action:web:/projects:handleCreate",
        }),
        expect.objectContaining({
          kind: "requests",
          from: "ui_action:web:/projects:handleCreate",
          to: expect.stringContaining("client_request:web:src/app/(dashboard)/projects/page.tsx"),
        }),
        expect.objectContaining({
          kind: "handles",
          to: "api_route:web:POST:/api/projects",
        }),
        expect.objectContaining({
          kind: "requests",
          from: "page_view:web:/events",
          to: expect.stringContaining("client_request:web:src/components/project-context.tsx:page:/events"),
        }),
        expect.objectContaining({
          kind: "handles",
          to: "api_route:web:GET:/api/projects/[projectId]/events",
        }),
      ])
    );
  });

  it("emits zero-coverage-inferred diagnostic when no patterns match", () => {
    const productDir = createProduct();
    writeFile(productDir, "src/app/page.tsx", `export default function HomePage() { return null; }`);
    writeFile(
      productDir,
      "__testkit__/misc.e2e.testkit.ts",
      `// no recognizable patterns here\nconsole.log("hello");`
    );

    const graph = buildCoverageGraph({
      productDir,
      services: {
        web: {
          local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
        },
      },
      discoveryFiles: [
        {
          serviceName: "web",
          type: "e2e",
          framework: "k6",
          suiteName: "misc",
          filePath: "__testkit__/misc.e2e.testkit.ts",
        },
      ],
    });

    expect(graph.diagnostics).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          level: "info",
          code: "zero-coverage-inferred",
          filePath: "__testkit__/misc.e2e.testkit.ts",
          service: "web",
        }),
      ])
    );

    const evidence = graph.evidence.find((e) => e.testFilePath === "__testkit__/misc.e2e.testkit.ts");
    expect(evidence.coveredNodeIds).toEqual([expect.stringContaining("test_file:web:")]);
  });

  it("does not exclude app/coverage source routes while still allowing top-level coverage output to be ignored", () => {
    const productDir = createProduct();
    writeFile(productDir, "app/coverage/page.tsx", `export default function CoveragePage() { return null; }`);
    writeFile(productDir, "coverage/page.tsx", `export default function OutputPage() { return null; }`);

    const context = buildServiceCoverageContext(
      productDir,
      "web",
      {
        local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
        discovery: { roots: ["app"] },
      },
      { exclude: ["coverage"] }
    );

    expect(context.pageByRoute.get("/coverage").node).toMatchObject({
      filePath: "app/coverage/page.tsx",
    });
    expect(context.graph.nodes.some((node) => node.filePath === "coverage/page.tsx")).toBe(false);
  });
});

function createProduct() {
  const productDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-coverage-"));
  cleanups.push(() => fs.rmSync(productDir, { recursive: true, force: true }));
  return productDir;
}

function writeFile(productDir, relativePath, content = "export {};\n") {
  const absolutePath = path.join(productDir, relativePath);
  fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
  fs.writeFileSync(absolutePath, content);
}
