import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { mkdtempSync, rmSync, readFileSync, existsSync, writeFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { tmpdir } from "node:os";

import { defaultStorePath } from "../src/store.js";
// The hooks `.mjs` twin of the data-root seam — SC8 binds it into the consistency check.
import { getDataRoot } from "../../../hooks/scripts/data-root.mjs";

// Package root (scripts/trends) so the wrapper path + `tsx` resolution are cwd-independent.
const trendsDir = fileURLToPath(new URL("..", import.meta.url));
const wrapper = join(trendsDir, "run-daily.sh");
const today = new Date().toISOString().slice(0, 10);

// Invoke THROUGH `bash` (never a direct executable spawn): an ABSENT wrapper exits 127
// (a clean assertion-RED), where a direct exec of a missing file would throw ENOENT /
// status:null (a module-not-found-class failure). Folded — plan-critic #7.
function runWrapper(
  args: string[],
  env: Record<string, string> = {},
  cwd: string = trendsDir,
): { status: number | null; stdout: string; stderr: string } {
  const res = spawnSync("bash", [wrapper, ...args], {
    input: "",
    encoding: "utf8",
    cwd,
    env: { ...process.env, ...env },
  });
  return { status: res.status, stdout: res.stdout, stderr: res.stderr };
}

describe("trends headless wrapper — run-daily.sh (RE-R3c / SC7, SC8)", () => {
  const freshIso = new Date(Date.now() - 2 * 86400000).toISOString().slice(0, 10);

  // One temp dir is BOTH the LINKEDIN_STUDIO_DATA root (so cron.log lands at <dir>/trends/cron.log)
  // AND holds the seeded store + brief out-dir, so the real per-user data dir is never touched.
  function fixture(): { dir: string; store: string; out: string } {
    const dir = mkdtempSync(join(tmpdir(), "r3c-wrap-"));
    const store = join(dir, "s.json");
    writeFileSync(
      store,
      JSON.stringify({
        schemaVersion: 2,
        trends: [
          {
            id: "a",
            title: "Fresh Match",
            url: "https://e/a",
            source: "tavily",
            capturedAt: freshIso,
            publishedAt: freshIso,
            topics: ["ai", "gov"],
          },
        ],
      }),
    );
    return { dir, store, out: join(dir, "mb") };
  }

  test("SC7: seeded store → dated brief .md written + exactly one compact cron.log line + exit 0", () => {
    const { dir, store, out } = fixture();
    try {
      const { status } = runWrapper(["--pillars", "ai,gov", "--store", store, "--out", out], {
        LINKEDIN_STUDIO_DATA: dir,
      });
      assert.equal(status, 0, "the deterministic brief exits 0");
      assert.ok(existsSync(join(out, `${today}.md`)), "the dated brief .md is written");
      const log = readFileSync(join(dir, "trends", "cron.log"), "utf8");
      const lines = log.split("\n").filter((l) => l.trim().length > 0);
      assert.equal(lines.length, 1, "exactly one log line (the pretty-printed brief json compacted to one line)");
      assert.match(lines[0], /^\S+ exit=0 /, "the line is `<ts> exit=0 <compact-json>`");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  test("SC7: a second same-day run → byte-identical .md, surfacedCount not double-counted, second log line", () => {
    const { dir, store, out } = fixture();
    try {
      assert.equal(runWrapper(["--pillars", "ai,gov", "--store", store, "--out", out], { LINKEDIN_STUDIO_DATA: dir }).status, 0);
      const md1 = readFileSync(join(out, `${today}.md`), "utf8");
      assert.equal(runWrapper(["--pillars", "ai,gov", "--store", store, "--out", out], { LINKEDIN_STUDIO_DATA: dir }).status, 0);
      const md2 = readFileSync(join(out, `${today}.md`), "utf8");
      assert.equal(md1, md2, "the same-day re-render is byte-identical");
      const persisted = JSON.parse(readFileSync(store, "utf8"));
      assert.equal(persisted.trends[0].surfacedCount, 1, "surfacedCount stays 1 (R3b per-day idempotency)");
      const lines = readFileSync(join(dir, "trends", "cron.log"), "utf8").split("\n").filter((l) => l.trim().length > 0);
      assert.equal(lines.length, 2, "two runs → two log lines");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  test("SC7: CWD-independent — runs from an unrelated cwd (the wrapper's `cd \"$DIR\"` resolves tsx)", () => {
    const { dir, store, out } = fixture();
    const otherCwd = mkdtempSync(join(tmpdir(), "r3c-cwd-"));
    try {
      const { status } = runWrapper(["--pillars", "ai,gov", "--store", store, "--out", out], { LINKEDIN_STUDIO_DATA: dir }, otherCwd);
      assert.equal(status, 0, "the wrapper resolves tsx from a cwd that is not the package dir");
      assert.ok(existsSync(join(out, `${today}.md`)), "the dated brief is still written");
    } finally {
      rmSync(dir, { recursive: true, force: true });
      rmSync(otherCwd, { recursive: true, force: true });
    }
  });

  test("SC8: data-path twin consistency — wrapper log dir == dirname(defaultStorePath()) == getDataRoot('trends')", () => {
    const { dir, store, out } = fixture();
    const saved = process.env.LINKEDIN_STUDIO_DATA;
    try {
      runWrapper(["--pillars", "ai,gov", "--store", store, "--out", out], { LINKEDIN_STUDIO_DATA: dir });
      const wrapperLogDir = join(dir, "trends"); // where the wrapper actually wrote cron.log
      assert.ok(existsSync(join(wrapperLogDir, "cron.log")), "the wrapper wrote into <data>/trends");

      // The TS store twin + the hooks `.mjs` twin, resolved under the SAME override.
      process.env.LINKEDIN_STUDIO_DATA = dir;
      assert.equal(dirname(defaultStorePath()), wrapperLogDir, "TS dirname(defaultStorePath()) == wrapper log dir");
      assert.equal(getDataRoot("trends"), wrapperLogDir, "hooks getDataRoot('trends') == wrapper log dir");

      // Override-independence: a different root still resolves the two TS/JS twins identically.
      const other = mkdtempSync(join(tmpdir(), "r3c-twin-"));
      process.env.LINKEDIN_STUDIO_DATA = other;
      assert.equal(dirname(defaultStorePath()), getDataRoot("trends"), "the twins agree for any override root");
      rmSync(other, { recursive: true, force: true });
    } finally {
      if (saved === undefined) delete process.env.LINKEDIN_STUDIO_DATA;
      else process.env.LINKEDIN_STUDIO_DATA = saved;
      rmSync(dir, { recursive: true, force: true });
    }
  });
});
