/**
 * Phase 7.1 R1 Agent C — Cold start Tier 1 regression tests.
 *
 * Covers the three optimisations introduced to close the R0 → R3 F cold
 * start regression (395 → 626 ms, +231 ms):
 *
 *   1. Boot parallelization in `cli/commands/dev.ts` — `validateAndReport`
 *      stays serial (seed), but `validateRuntimeLockfile` + `loadEnv` run
 *      through `Promise.allSettled`. Savings: ~40-70 ms (diagnostic §3.A).
 *   2. `startSqliteStore` fire-and-forget — boot no longer blocks on the
 *      observability store's dynamic `bun:sqlite` import + schema. Savings:
 *      ~20-40 ms (diagnostic §3.B).
 *   3. Per-island conditional skip — `scanIslandFiles` defensively re-
 *      filters its input via `needsHydration`, so a caller that forgets
 *      to pre-filter does not readdir every page route. Savings: ~40-70 ms
 *      on mixed-hydration projects (diagnostic §5 Tier 1 item 3).
 *
 * Plus: pins the 9 new B_gap `HMR_PERF` markers (BOOT_VALIDATE_CONFIG
 * through BOOT_WATCH_FS_ROUTES) so Agent D's perf-regression fixture
 * can match on them without ad-hoc string literals.
 *
 * References:
 *   docs/bun/phase-7-1-diagnostics/cold-start-breakdown.md §§3, 5, 7
 *   docs/bun/phase-7-1-team-plan.md §4 Agent C
 *   packages/core/src/perf/hmr-markers.ts BOOT_* markers
 */
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
import { tmpdir } from "os";
import path from "path";
import type { RouteSpec, RoutesManifest } from "../../spec/schema";
import {
  _testOnly_scanIslandFiles,
  _testOnly_scanPartialFiles,
  _testOnly_getHydratedRoutes,
  _testOnly_getHydrationRoutesMissingClientModule,
} from "../build";
import { HMR_PERF } from "../../perf/hmr-markers";
import {
  _resetCacheForTesting as _resetPerfCache,
  isPerfEnabled,
  mark,
  measure,
  withPerf,
} from "../../perf";

// -----------------------------------------------------------------------------
// Helpers — minimal RouteSpec factories.
// -----------------------------------------------------------------------------

/** Produce a hydrated page route spec rooted at `dir` (relative). */
function pageRoute(
  id: string,
  pattern: string,
  dir: string,
  opts: { hydration?: "visible" | "idle" | "immediate" | "never" | "none" } = {},
): RouteSpec {
  const hydration =
    opts.hydration === undefined
      ? undefined
      : opts.hydration === "none" || opts.hydration === "never"
        ? { strategy: "none" as const }
        : { strategy: opts.hydration as "visible" | "idle" | "immediate" };
  return {
    id,
    pattern,
    kind: "page",
    module: `${dir}/page.tsx`,
    componentModule: `${dir}/page.tsx`,
    clientModule: `${dir}/page.tsx`,
    ...(hydration ? { hydration } : {}),
  } as RouteSpec;
}

/** Produce a pure-SSR page route (hydration.strategy === "none"). */
function pureSsrRoute(id: string, pattern: string, dir: string): RouteSpec {
  return {
    id,
    pattern,
    kind: "page",
    module: `${dir}/page.tsx`,
    componentModule: `${dir}/page.tsx`,
    hydration: { strategy: "none" },
  } as RouteSpec;
}

/** Produce an API route spec (never hydrated). */
function apiRoute(id: string, pattern: string, dir: string): RouteSpec {
  return {
    id,
    pattern,
    kind: "api",
    module: `${dir}/route.ts`,
  } as RouteSpec;
}

/** Materialise an app/ tree on disk so `scanIslandFiles` can readdir it. */
function createProject(): {
  rootDir: string;
  writeIslandFile: (dir: string, fileName: string) => void;
} {
  const rootDir = mkdtempSync(path.join(tmpdir(), "mandu-coldstart-"));
  const writeIslandFile = (dir: string, fileName: string): void => {
    const abs = path.join(rootDir, dir);
    mkdirSync(abs, { recursive: true });
    writeFileSync(
      path.join(abs, fileName),
      `export default function X(){return null}\n`,
    );
  };
  return { rootDir, writeIslandFile };
}

// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------

describe("Phase 7.1 R1 Agent C — B_gap markers (9 boot stages)", () => {
  it("defines BOOT_VALIDATE_CONFIG", () => {
    expect(HMR_PERF.BOOT_VALIDATE_CONFIG).toBe("boot:validate-config");
  });

  it("defines BOOT_LOCKFILE_CHECK", () => {
    expect(HMR_PERF.BOOT_LOCKFILE_CHECK).toBe("boot:lockfile-check");
  });

  it("defines BOOT_LOAD_ENV", () => {
    expect(HMR_PERF.BOOT_LOAD_ENV).toBe("boot:load-env");
  });

  it("defines BOOT_SQLITE_START", () => {
    expect(HMR_PERF.BOOT_SQLITE_START).toBe("boot:sqlite-start");
  });

  it("defines BOOT_GUARD_PREFLIGHT", () => {
    expect(HMR_PERF.BOOT_GUARD_PREFLIGHT).toBe("boot:guard-preflight");
  });

  it("defines BOOT_RESOLVE_PORT", () => {
    expect(HMR_PERF.BOOT_RESOLVE_PORT).toBe("boot:resolve-port");
  });

  it("defines BOOT_HMR_SERVER", () => {
    expect(HMR_PERF.BOOT_HMR_SERVER).toBe("boot:hmr-server");
  });

  it("defines BOOT_START_SERVER", () => {
    expect(HMR_PERF.BOOT_START_SERVER).toBe("boot:start-server");
  });

  it("defines BOOT_WATCH_FS_ROUTES", () => {
    expect(HMR_PERF.BOOT_WATCH_FS_ROUTES).toBe("boot:watch-fs-routes");
  });

  it("all 9 markers share the `boot:` namespace", () => {
    // Consistency check so log aggregation tooling can grep for `boot:`.
    const bootKeys = [
      HMR_PERF.BOOT_VALIDATE_CONFIG,
      HMR_PERF.BOOT_LOCKFILE_CHECK,
      HMR_PERF.BOOT_LOAD_ENV,
      HMR_PERF.BOOT_SQLITE_START,
      HMR_PERF.BOOT_GUARD_PREFLIGHT,
      HMR_PERF.BOOT_RESOLVE_PORT,
      HMR_PERF.BOOT_HMR_SERVER,
      HMR_PERF.BOOT_START_SERVER,
      HMR_PERF.BOOT_WATCH_FS_ROUTES,
    ];
    for (const key of bootKeys) {
      expect(key.startsWith("boot:")).toBe(true);
    }
    // No duplicate values — a typo renaming one to match another would
    // break per-stage accounting.
    const unique = new Set(bootKeys);
    expect(unique.size).toBe(bootKeys.length);
  });
});

describe("Phase 7.1 R1 Agent C — perf markers fire when MANDU_PERF=1", () => {
  const originalPerf = process.env.MANDU_PERF;

  beforeEach(() => {
    process.env.MANDU_PERF = "1";
    _resetPerfCache();
  });

  afterEach(() => {
    if (originalPerf === undefined) delete process.env.MANDU_PERF;
    else process.env.MANDU_PERF = originalPerf;
    _resetPerfCache();
  });

  it("isPerfEnabled reflects MANDU_PERF=1", () => {
    expect(isPerfEnabled()).toBe(true);
  });

  it("mark/measure round-trips through all 9 BOOT markers without throwing", () => {
    // Exercises every marker — ensures none collide with a previously
    // reserved key (e.g. accidental reuse of `ssr:bundled-import`).
    const markers = [
      HMR_PERF.BOOT_VALIDATE_CONFIG,
      HMR_PERF.BOOT_LOCKFILE_CHECK,
      HMR_PERF.BOOT_LOAD_ENV,
      HMR_PERF.BOOT_SQLITE_START,
      HMR_PERF.BOOT_GUARD_PREFLIGHT,
      HMR_PERF.BOOT_RESOLVE_PORT,
      HMR_PERF.BOOT_HMR_SERVER,
      HMR_PERF.BOOT_START_SERVER,
      HMR_PERF.BOOT_WATCH_FS_ROUTES,
    ];
    for (const m of markers) {
      mark(m);
      const ms = measure(m, m);
      // Non-negative; may be 0 if the mark/measure pair runs within the
      // same nanosecond tick on very fast hosts.
      expect(ms).toBeGreaterThanOrEqual(0);
    }
  });

  it("withPerf wraps async work with a BOOT marker", async () => {
    // Mirrors the production usage at
    // `cli/src/commands/dev.ts:73` (BOOT_VALIDATE_CONFIG wrapper).
    const result = await withPerf(HMR_PERF.BOOT_VALIDATE_CONFIG, async () => {
      // simulate a tiny async boot task
      await new Promise((resolve) => setTimeout(resolve, 1));
      return 42;
    });
    expect(result).toBe(42);
  });
});

describe("Phase 7.1 R1 Agent C — boot parallelization semantics", () => {
  // Unit tests that pin the Promise.allSettled / Promise.all choice and
  // ordering contract described in dev.ts:89-112. Integration coverage
  // (spawning real `mandu dev`) lives in `scripts/hmr-bench.ts`.

  it("Promise.allSettled surfaces BOTH rejections without short-circuit", async () => {
    // Guards against an accidental refactor to Promise.all, which would
    // swallow the second task's result on the first rejection. The boot
    // path needs env failures to be warned-about, not hidden behind a
    // lockfile error.
    const lockReject = Promise.reject(new Error("lockfile corrupt"));
    const envReject = Promise.reject(new Error("env malformed"));
    const [a, b] = await Promise.allSettled([lockReject, envReject]);
    expect(a.status).toBe("rejected");
    expect(b.status).toBe("rejected");
  });

  it("Promise.allSettled resolves one side while the other rejects", async () => {
    // The envResult advisory branch in dev.ts:168-176 must still fire
    // when the lockfile rejects. This test pins the allSettled contract
    // so a `Promise.all` regression cannot quietly mask env warnings.
    const lockOk = Promise.resolve({ lockfile: null });
    const envReject = Promise.reject(new Error("env cooked"));
    const [a, b] = await Promise.allSettled([lockOk, envReject]);
    expect(a.status).toBe("fulfilled");
    expect(b.status).toBe("rejected");
  });

  it("fire-and-forget Promise does not block the boot path", async () => {
    // Models the `sqliteStorePromise` pattern in dev.ts:120-138. The
    // "boot" side must resolve immediately even if the background task
    // is still pending.
    let backgroundDone = false;
    const bgPromise = new Promise<void>((resolve) => {
      setTimeout(() => {
        backgroundDone = true;
        resolve();
      }, 100);
    }).catch(() => {});

    const bootStart = Date.now();
    // Simulate boot continuing past the SQLite start mark without
    // awaiting bgPromise.
    const bootElapsed = Date.now() - bootStart;
    expect(bootElapsed).toBeLessThan(50); // < 50ms — not blocked

    // Cleanup — wait for the background task so the test suite doesn't
    // leak an unresolved promise into the next case.
    await bgPromise;
    expect(backgroundDone).toBe(true);
  });

  it("fire-and-forget caught rejection does not bubble up", async () => {
    // Pins the `.catch(() => {})` behaviour on `sqliteStorePromise` so
    // boot never aborts on a missing bun:sqlite.
    const rejected = Promise.reject(new Error("bun:sqlite unavailable")).catch(
      () => {
        /* swallow */
      },
    );
    // Should resolve to undefined without rethrowing.
    await expect(rejected).resolves.toBeUndefined();
  });
});

describe("Phase 7.1 R1 Agent C — getHydratedRoutes filter", () => {
  // These pin the existing filter so Agent B's Fast Refresh changes in
  // `buildPerIslandBundle` cannot accidentally re-enable hydration on
  // pure-SSR routes.

  it("keeps pages with default hydration (no explicit config)", () => {
    const route = pageRoute("home", "/", "app");
    const manifest = { version: 1, routes: [route] };
    const hydrated = _testOnly_getHydratedRoutes(manifest);
    expect(hydrated).toHaveLength(1);
    expect(hydrated[0].id).toBe("home");
  });

  it("keeps pages with explicit visible/idle/immediate strategies", () => {
    const manifest = {
      version: 1,
      routes: [
        pageRoute("a", "/a", "app/a", { hydration: "visible" }),
        pageRoute("b", "/b", "app/b", { hydration: "idle" }),
        pageRoute("c", "/c", "app/c", { hydration: "immediate" }),
      ],
    };
    const hydrated = _testOnly_getHydratedRoutes(manifest);
    expect(hydrated).toHaveLength(3);
  });

  it("drops pages with hydration.strategy === 'none'", () => {
    // pureSsrRoute has no clientModule — we construct with clientModule
    // to cover the filter's clientModule-guard separately below.
    const manifest = {
      version: 1,
      routes: [pureSsrRoute("static", "/static", "app/static")],
    };
    const hydrated = _testOnly_getHydratedRoutes(manifest);
    expect(hydrated).toHaveLength(0);
  });

  it("drops pages without a clientModule", () => {
    const route: RouteSpec = {
      id: "api-like-page",
      pattern: "/foo",
      kind: "page",
      module: "app/foo/page.tsx",
      componentModule: "app/foo/page.tsx",
      // No clientModule — should NOT get an island bundle.
    } as RouteSpec;
    const manifest = { version: 1, routes: [route] };
    const hydrated = _testOnly_getHydratedRoutes(manifest);
    expect(hydrated).toHaveLength(0);
  });

  it("drops API routes entirely", () => {
    const manifest = {
      version: 1,
      routes: [
        pageRoute("home", "/", "app"),
        apiRoute("users", "/api/users", "app/api/users"),
      ],
    };
    const hydrated = _testOnly_getHydratedRoutes(manifest);
    expect(hydrated).toHaveLength(1);
    expect(hydrated[0].id).toBe("home");
  });

  it("mixed manifest: drops pure-SSR, keeps hydrated", () => {
    const manifest = {
      version: 1,
      routes: [
        pageRoute("home", "/", "app", { hydration: "visible" }),
        pureSsrRoute("about", "/about", "app/about"),
        pageRoute("dashboard", "/dashboard", "app/dashboard", {
          hydration: "idle",
        }),
        apiRoute("health", "/api/health", "app/api/health"),
      ],
    };
    const hydrated = _testOnly_getHydratedRoutes(manifest);
    expect(hydrated.map((r) => r.id).sort()).toEqual(["dashboard", "home"]);
  });
});

describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", () => {
  let project: ReturnType<typeof createProject>;

  beforeEach(() => {
    project = createProject();
  });

  afterEach(() => {
    try {
      rmSync(project.rootDir, { recursive: true, force: true });
    } catch {
      /* Windows lock tolerance */
    }
  });

  it("skips a route with hydration.strategy === 'none' even if passed explicitly", async () => {
    // Regression guard — the task spec says "이미 되고 있을 수 있음 (확인 필요)".
    // We pin that scanIslandFiles itself (not just its callers) rejects
    // pure-SSR routes via the defensive `needsHydration` check.
    project.writeIslandFile("app/static", "page.tsx");
    project.writeIslandFile("app/static", "widget.island.tsx");

    const route = pureSsrRoute("static", "/static", "app/static");
    const result = await _testOnly_scanIslandFiles([route], project.rootDir);

    // Because the route is pure-SSR, the widget.island.tsx is ignored —
    // nothing to hydrate. This also saves a readdir on the app/static
    // directory (the -40-70 ms Tier 1 optimisation).
    expect(result).toHaveLength(0);
  });

  it("scans directories of hydrated routes and picks up *.island.tsx files", async () => {
    project.writeIslandFile("app", "page.tsx");
    project.writeIslandFile("app", "counter.island.tsx");
    project.writeIslandFile("app", "banner.island.ts");

    const route = pageRoute("home", "/", "app", { hydration: "visible" });
    const result = await _testOnly_scanIslandFiles([route], project.rootDir);

    expect(result).toHaveLength(2);
    expect(result.map((e) => e.name).sort()).toEqual(["banner", "counter"]);
    for (const entry of result) {
      expect(entry.routeId).toBe("home");
      expect(entry.priority).toBeDefined();
    }
  });

  it("dedupes directory scans when multiple hydrated routes share a dir", async () => {
    // Same dir = seenDirs filter should kick in after the first scan.
    // We verify via the number of entries — a duplicate scan would
    // return 2 entries per island file.
    project.writeIslandFile("app", "page.tsx");
    project.writeIslandFile("app", "counter.island.tsx");

    const r1 = pageRoute("home", "/", "app", { hydration: "visible" });
    const r2 = pageRoute("alt", "/alt", "app", { hydration: "idle" });
    const result = await _testOnly_scanIslandFiles([r1, r2], project.rootDir);

    expect(result).toHaveLength(1);
    // First-encountered route wins (seenDirs short-circuits subsequent).
    expect(result[0].routeId).toBe("home");
  });

  it("skips pure-SSR routes interleaved between hydrated routes", async () => {
    // Mixed-hydration project — the defensive skip saves an fs.readdir
    // on the pure-SSR branch. This is the canonical §5 Tier 1 win.
    project.writeIslandFile("app", "page.tsx");
    project.writeIslandFile("app", "home.island.tsx");
    project.writeIslandFile("app/about", "page.tsx");
    // about has NO island files AND is pure-SSR — should not be readdir'd.
    project.writeIslandFile("app/dashboard", "page.tsx");
    project.writeIslandFile("app/dashboard", "widget.island.tsx");

    const routes = [
      pageRoute("home", "/", "app", { hydration: "visible" }),
      pureSsrRoute("about", "/about", "app/about"),
      pageRoute("dashboard", "/dashboard", "app/dashboard", {
        hydration: "idle",
      }),
    ];
    const result = await _testOnly_scanIslandFiles(routes, project.rootDir);

    expect(result.map((e) => e.name).sort()).toEqual(["home", "widget"]);
    // about is not even attempted — no error, no scan entry.
    expect(result.some((e) => e.routeId === "about")).toBe(false);
  });

  it("handles missing route directories gracefully (fs.readdir catch)", async () => {
    // An orphan route — the readdir will throw ENOENT. The scanner
    // must swallow it and continue with later routes.
    project.writeIslandFile("app", "page.tsx");
    project.writeIslandFile("app", "counter.island.tsx");

    const routes = [
      pageRoute("missing", "/missing", "app/nonexistent", {
        hydration: "visible",
      }),
      pageRoute("home", "/", "app", { hydration: "visible" }),
    ];
    const result = await _testOnly_scanIslandFiles(routes, project.rootDir);

    expect(result).toHaveLength(1);
    expect(result[0].name).toBe("counter");
  });

  it("returns empty array for all-empty input (no hydrated routes)", async () => {
    // Guards the "hydratedRoutes.length === 0" fast path — even if a
    // caller bypasses the early-return at build.ts:1478 and calls the
    // scanner directly with no hydrated routes, we still return [].
    const result = await _testOnly_scanIslandFiles([], project.rootDir);
    expect(result).toEqual([]);
  });

  it("returns empty array when ALL input routes are pure-SSR", async () => {
    // §5 Tier 1 item 3 — the "per-island conditional skip" big win.
    // When no routes need hydration, scanIslandFiles must be a pure
    // no-op (no fs.readdir calls), not just return [].
    project.writeIslandFile("app/a", "page.tsx");
    project.writeIslandFile("app/a", "x.island.tsx");
    project.writeIslandFile("app/b", "page.tsx");
    project.writeIslandFile("app/b", "y.island.tsx");

    const routes = [
      pureSsrRoute("a", "/a", "app/a"),
      pureSsrRoute("b", "/b", "app/b"),
    ];
    const result = await _testOnly_scanIslandFiles(routes, project.rootDir);
    expect(result).toEqual([]);
  });
});

describe("hydration route validation", () => {
  it("finds pages that request hydration without a client module", () => {
    const manifest = {
      version: 1,
      routes: [
        {
          id: "login",
          pattern: "/login",
          kind: "page",
          module: "app/login/page.tsx",
          componentModule: "app/login/page.tsx",
          hydration: { strategy: "full", priority: "immediate", preload: false },
        },
        pureSsrRoute("about", "/about", "app/about"),
        pageRoute("dashboard", "/dashboard", "app/dashboard"),
      ],
    } as RoutesManifest;

    const missing = _testOnly_getHydrationRoutesMissingClientModule(manifest);
    expect(missing.map((route) => route.id)).toEqual(["login"]);
  });
});

describe("partial bundle scan", () => {
  let project: ReturnType<typeof createProject>;

  beforeEach(() => {
    project = createProject();
  });

  afterEach(() => {
    try {
      rmSync(project.rootDir, { recursive: true, force: true });
    } catch {
      /* Windows lock tolerance */
    }
  });

  it("discovers *.partial.tsx files outside .mandu and node_modules", async () => {
    project.writeIslandFile("app", "Home.partial.tsx");
    project.writeIslandFile(".mandu/client", "Stale.partial.tsx");
    project.writeIslandFile("node_modules/pkg", "Ignored.partial.tsx");

    const result = await _testOnly_scanPartialFiles(project.rootDir);

    expect(result.map((entry) => entry.name)).toEqual(["Home"]);
    expect(result[0].priority).toBe("visible");
  });
});
