import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { suggestSlashCommands } from "../src/cli/ui/slash.js";
import { loadSlashUsage, recordSlashUse, slashUsagePath } from "../src/slash-usage.js";

let dir: string;
let prevCarbonEnv: string | undefined;
let prevReasonixEnv: string | undefined;

beforeEach(() => {
  dir = mkdtempSync(join(tmpdir(), "carbon-usage-"));
  prevCarbonEnv = process.env.CARBONCODE_SLASH_USAGE_PATH;
  prevReasonixEnv = process.env.REASONIX_SLASH_USAGE_PATH;
  process.env.CARBONCODE_SLASH_USAGE_PATH = join(dir, "slash-usage.json");
  // biome-ignore lint/performance/noDelete: test controls legacy env fallback explicitly
  delete process.env.REASONIX_SLASH_USAGE_PATH;
});

afterEach(() => {
  if (prevCarbonEnv === undefined) {
    // biome-ignore lint/performance/noDelete: process.env must lose the key, not hold "undefined"
    delete process.env.CARBONCODE_SLASH_USAGE_PATH;
  } else {
    process.env.CARBONCODE_SLASH_USAGE_PATH = prevCarbonEnv;
  }
  if (prevReasonixEnv === undefined) {
    // biome-ignore lint/performance/noDelete: process.env must lose the key, not hold "undefined"
    delete process.env.REASONIX_SLASH_USAGE_PATH;
  } else {
    process.env.REASONIX_SLASH_USAGE_PATH = prevReasonixEnv;
  }
  rmSync(dir, { recursive: true, force: true });
});

describe("slash-usage store", () => {
  it("returns empty when the file doesn't exist yet", () => {
    expect(loadSlashUsage()).toEqual({});
  });

  it("defaults to ~/.carboncode/slash-usage.json when no override is set", () => {
    // biome-ignore lint/performance/noDelete: this test exercises default path resolution
    delete process.env.CARBONCODE_SLASH_USAGE_PATH;
    // biome-ignore lint/performance/noDelete: this test exercises default path resolution
    delete process.env.REASONIX_SLASH_USAGE_PATH;
    expect(slashUsagePath()).toMatch(/[/\\]\.carboncode[/\\]slash-usage\.json$/);
    expect(slashUsagePath()).not.toContain(".reasonix");
  });

  it("keeps REASONIX_SLASH_USAGE_PATH as a legacy override", () => {
    // biome-ignore lint/performance/noDelete: legacy fallback must be explicit
    delete process.env.CARBONCODE_SLASH_USAGE_PATH;
    process.env.REASONIX_SLASH_USAGE_PATH = join(dir, "legacy-slash-usage.json");
    expect(slashUsagePath()).toBe(join(dir, "legacy-slash-usage.json"));
  });

  it("trims CARBONCODE_SLASH_USAGE_PATH before using it", () => {
    const override = join(dir, "trimmed-slash-usage.json");
    process.env.CARBONCODE_SLASH_USAGE_PATH = ` ${override} `;
    expect(slashUsagePath()).toBe(override);
  });

  it("trims legacy REASONIX_SLASH_USAGE_PATH before using it", () => {
    const override = join(dir, "trimmed-legacy-slash-usage.json");
    // biome-ignore lint/performance/noDelete: legacy fallback must be explicit
    delete process.env.CARBONCODE_SLASH_USAGE_PATH;
    process.env.REASONIX_SLASH_USAGE_PATH = ` ${override} `;
    expect(slashUsagePath()).toBe(override);
  });

  it("recordSlashUse persists to disk and survives reload", () => {
    recordSlashUse("status");
    recordSlashUse("status");
    recordSlashUse("compact");
    expect(loadSlashUsage()).toEqual({ status: 2, compact: 1 });
    expect(existsSync(slashUsagePath())).toBe(true);
  });

  it("merges concurrent counts via read-modify-write", () => {
    recordSlashUse("status");
    writeFileSync(
      slashUsagePath(),
      JSON.stringify({ version: 1, counts: { status: 5, retry: 3 } }),
      "utf8",
    );
    const after = recordSlashUse("status");
    expect(after.status).toBe(6);
    expect(after.retry).toBe(3);
  });

  it("ignores garbage payloads instead of crashing", () => {
    writeFileSync(slashUsagePath(), "not-json", "utf8");
    expect(loadSlashUsage()).toEqual({});
    writeFileSync(slashUsagePath(), JSON.stringify({ counts: { x: "lol" } }), "utf8");
    expect(loadSlashUsage()).toEqual({});
    writeFileSync(slashUsagePath(), JSON.stringify({ counts: { x: -5 } }), "utf8");
    expect(loadSlashUsage()).toEqual({});
  });

  it("writes a stable on-disk shape", () => {
    recordSlashUse("status");
    const raw = JSON.parse(readFileSync(slashUsagePath(), "utf8"));
    expect(raw).toEqual({ version: 1, counts: { status: 1 } });
  });
});

describe("suggestSlashCommands frequency sort", () => {
  it("preserves declared order when no counts are passed", () => {
    const cmds = suggestSlashCommands("h").map((s) => s.cmd);
    expect(cmds).toEqual(["help", "hooks"]);
  });

  it("sorts higher-count commands first within the filtered set", () => {
    const sorted = suggestSlashCommands("h", false, { hooks: 50, help: 1 }).map((s) => s.cmd);
    expect(sorted[0]).toBe("hooks");
    expect(sorted).toContain("help");
  });

  it("falls back to declared order when counts tie", () => {
    const sorted = suggestSlashCommands("h", false, {}).map((s) => s.cmd);
    expect(sorted).toEqual(["help", "hooks"]);
  });

  it("ignores counts for commands outside the filter set", () => {
    const sorted = suggestSlashCommands("h", false, { status: 9999 }).map((s) => s.cmd);
    expect(sorted).toEqual(["help", "hooks"]);
  });
});
