import { describe, it, expect, afterEach } from "vitest";
import * as fs from "node:fs";
import * as fsp from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import devLogger, { stripAnsi } from "./devLogger";

const makeTempPath = (name: string) =>
  path.join(os.tmpdir(), `devlogger-${process.pid}-${Date.now()}-${name}`);

const readNdjson = (file: string): Record<string, unknown>[] => {
  const raw = fs.readFileSync(file, "utf8");
  return raw
    .split("\n")
    .filter((l) => l.length > 0)
    .map((l) => JSON.parse(l));
};

const tracked: string[] = [];
const trackedFile = (name: string) => {
  const p = makeTempPath(name);
  tracked.push(p);
  return p;
};

afterEach(async () => {
  await devLogger.close();
  while (tracked.length) {
    const p = tracked.pop()!;
    try {
      await fsp.unlink(p);
    } catch {
      // ignore
    }
  }
});

describe("stripAnsi", () => {
  it("removes CSI escape sequences", () => {
    expect(stripAnsi("\x1B[31mhello\x1B[0m")).toBe("hello");
    expect(stripAnsi("\x1B[2K\x1B[1G✔ done")).toBe("✔ done");
  });

  it("leaves plain text untouched", () => {
    expect(stripAnsi("plain")).toBe("plain");
    expect(stripAnsi("")).toBe("");
  });

  it("accepts buffers", () => {
    expect(stripAnsi(Buffer.from("\x1B[31mhi\x1B[0m"))).toBe("hi");
  });
});

describe("devLogger no-op when no flags", () => {
  it("does not emit anything when events-file is unset", async () => {
    const before = process.stdout.write;
    await devLogger.init({});
    devLogger.marker("anything");
    devLogger.issue({
      scope: "embeddable",
      filePath: "/x.yml",
      message: "noop",
    });
    devLogger.startCycle("embeddable");
    devLogger.endCycle(1, "embeddable", "ok");
    expect(process.stdout.write).toBe(before);
    await devLogger.close();
  });
});

describe("devLogger events file", () => {
  it("writes one valid JSON object per line for marker and issue", async () => {
    const eventsFile = trackedFile("events.ndjson");
    await devLogger.init({ eventsFile });

    devLogger.marker("custom_event", { scope: "embeddable", foo: "bar" });
    devLogger.issue({
      scope: "embeddable",
      stage: "validate",
      filePath: "/abs/foo.embeddable.yml",
      message: "Required",
      line: 12,
      column: 3,
      path: "embeddables[0].name",
    });

    await devLogger.close();
    const lines = readNdjson(eventsFile);
    expect(lines).toHaveLength(2);

    const [marker, issue] = lines;
    expect(marker).toMatchObject({
      type: "marker",
      event: "custom_event",
      scope: "embeddable",
      foo: "bar",
    });
    expect(typeof marker.ts).toBe("string");

    expect(issue).toMatchObject({
      type: "issue",
      event: "validation_error",
      scope: "embeddable",
      stage: "validate",
      file: "/abs/foo.embeddable.yml",
      message: "Required",
      line: 12,
      col: 3,
      path: "embeddables[0].name",
    });
  });

  it("emits matched validate_start/validate_end with the same cycle id", async () => {
    const eventsFile = trackedFile("cycle.ndjson");
    await devLogger.init({ eventsFile });

    const cycle = devLogger.startCycle("embeddable", { files: ["/a.yml"] });
    devLogger.endCycle(cycle, "embeddable", "ok");

    await devLogger.close();
    const lines = readNdjson(eventsFile);
    expect(lines).toHaveLength(2);
    expect(lines[0]).toMatchObject({
      type: "marker",
      event: "validate_start",
      scope: "embeddable",
      cycle,
      files: ["/a.yml"],
    });
    expect(lines[1]).toMatchObject({
      type: "marker",
      event: "validate_end",
      scope: "embeddable",
      cycle,
      status: "ok",
    });
  });

  it("preserves monotonic cycle ids across consecutive cycles", async () => {
    const eventsFile = trackedFile("monotonic.ndjson");
    await devLogger.init({ eventsFile });

    const c1 = devLogger.startCycle("embeddable");
    devLogger.endCycle(c1, "embeddable", "ok");
    const c2 = devLogger.startCycle("embeddable");
    devLogger.endCycle(c2, "embeddable", "error", { stage: "validate" });

    expect(c2).toBe(c1 + 1);
    await devLogger.close();
  });
});

describe("devLogger TTY mirror", () => {
  it("patches stdout/stderr while a log file is active and writes ANSI-stripped copy", async () => {
    const logFile = trackedFile("mirror.log");
    const origStdout = process.stdout.write;
    const origStderr = process.stderr.write;

    await devLogger.init({ logFile });
    expect(process.stdout.write).not.toBe(origStdout);
    expect(process.stderr.write).not.toBe(origStderr);

    process.stdout.write("\x1B[31mhello\x1B[0m\n");
    process.stderr.write("\x1B[2K\x1B[1G✔ done\n");

    await devLogger.close();
    expect(process.stdout.write).toBe(origStdout);
    expect(process.stderr.write).toBe(origStderr);

    const content = fs.readFileSync(logFile, "utf8");
    expect(content).toContain("hello\n");
    expect(content).toContain("✔ done\n");
    expect(content).not.toContain("\x1B");
  });

  it("does not patch stdout when only events-file is set", async () => {
    const eventsFile = trackedFile("events-only.ndjson");
    const origStdout = process.stdout.write;
    await devLogger.init({ eventsFile });
    expect(process.stdout.write).toBe(origStdout);
    await devLogger.close();
  });
});
