import assert from "node:assert/strict";
import { chmodSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
import codexFastModeExtension, {
  FAST_MODE_SERVICE_TIER,
  FAST_MODE_STATE_ENTRY_TYPE,
  FAST_MODE_STATUS_KEY,
  fastModeArgumentCompletions,
  getFastModeConfigPath,
  isFastModeEligibleModel,
  isPlainObject,
  loadGlobalFastModeState,
  parseFastModeCommand,
  reconstructFastModeState,
  saveGlobalFastModeState,
  transformFastModeRequest,
  type FastModeState,
} from "../index.ts";

type StatusUpdate = { key: string; value: string | undefined };
type Notification = { message: string; level: string };
type Handler = (event: any, ctx: any) => unknown;

function createHarness(options: {
  entries?: unknown[];
  busy?: boolean;
  pending?: boolean;
  globalEnabled?: boolean;
  saveError?: string;
} = {}) {
  const handlers = new Map<string, Handler>();
  const commands = new Map<string, any>();
  const entries = options.entries ?? [];
  const statusUpdates: StatusUpdate[] = [];
  const notifications: Notification[] = [];
  const appendCalls: Array<{ customType: string; data: unknown }> = [];
  const loadCalls: string[] = [];
  const saveCalls: Array<{ path: string; state: FastModeState }> = [];
  let busy = options.busy ?? false;
  let pending = options.pending ?? false;
  let globalEnabled = options.globalEnabled ?? false;

  const context = {
    model: { provider: "openai-codex", api: "openai-codex-responses" },
    sessionManager: { getBranch: () => entries },
    isIdle: () => !busy,
    hasPendingMessages: () => pending,
    ui: {
      setStatus(key: string, value: string | undefined) {
        statusUpdates.push({ key, value });
      },
      notify(message: string, level: string) {
        notifications.push({ message, level });
      },
    },
  } as unknown as ExtensionCommandContext;

  codexFastModeExtension({
    on(event: string, handler: Handler) {
      handlers.set(event, handler);
    },
    registerCommand(name: string, command: unknown) {
      commands.set(name, command);
    },
    appendEntry(customType: string, data: unknown) {
      appendCalls.push({ customType, data });
      entries.push({ type: "custom", customType, data });
    },
  } as unknown as ExtensionAPI, {
    configPath: "/test-agent/extensions/pi-extension-codex-fast-mode/config.json",
    loadGlobalState(path) {
      loadCalls.push(path);
      return { state: { enabled: globalEnabled } };
    },
    saveGlobalState(path, state) {
      saveCalls.push({ path, state });
      if (options.saveError) return { success: false, error: options.saveError };
      globalEnabled = state.enabled;
      return { success: true };
    },
  });

  return {
    appendCalls,
    commands,
    context,
    entries,
    handlers,
    loadCalls,
    notifications,
    saveCalls,
    setBusy(value: boolean) { busy = value; },
    setPending(value: boolean) { pending = value; },
    statusUpdates,
  };
}

test("plain-object guard excludes malformed payloads", () => {
  assert.equal(isPlainObject({}), true);
  assert.equal(isPlainObject(Object.create(null)), true);
  assert.equal(isPlainObject([]), false);
  assert.equal(isPlainObject(new Date()), false);
  assert.equal(isPlainObject(null), false);
  assert.equal(isPlainObject("payload"), false);
});

test("provider eligibility is exact", () => {
  assert.equal(isFastModeEligibleModel({ provider: "openai-codex", api: "openai-codex-responses" }), true);
  assert.equal(isFastModeEligibleModel({ provider: "openai", api: "openai-codex-responses" }), false);
  assert.equal(isFastModeEligibleModel({ provider: "openai-codex", api: "openai-responses" }), false);
  assert.equal(isFastModeEligibleModel(undefined), false);
});

test("request transformer is isolated, non-mutating, and overwrites only service_tier", () => {
  const payload = { model: "gpt-5.6-codex", service_tier: "default", nested: { preserved: true } };
  const transformed = transformFastModeRequest(true, { provider: "openai-codex", api: "openai-codex-responses" }, payload);

  assert.deepEqual(transformed, {
    model: "gpt-5.6-codex",
    service_tier: FAST_MODE_SERVICE_TIER,
    nested: { preserved: true },
  });
  assert.notStrictEqual(transformed, payload);
  assert.strictEqual(transformed?.nested, payload.nested);
  assert.equal(payload.service_tier, "default");
  assert.equal(transformFastModeRequest(false, { provider: "openai-codex", api: "openai-codex-responses" }, payload), undefined);
  assert.equal(transformFastModeRequest(true, { provider: "openai", api: "openai-codex-responses" }, payload), undefined);
  assert.equal(transformFastModeRequest(true, { provider: "openai-codex", api: "openai-codex-responses" }, []), undefined);
});

test("branch reconstruction prefers the latest valid snapshot over the global default", () => {
  assert.deepEqual(reconstructFastModeState([], true), { enabled: true });
  assert.deepEqual(reconstructFastModeState([
    { type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: true } },
    { type: "custom", customType: "other", data: { enabled: false } },
    { type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: "invalid" } },
    { type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: false } },
  ], true), { enabled: false });
});

test("global config round-trips atomically with owner-only permissions", () => {
  const agentDir = mkdtempSync(join(tmpdir(), "fast-mode-test-"));
  const configPath = getFastModeConfigPath(agentDir);
  try {
    assert.deepEqual(loadGlobalFastModeState(configPath), { state: { enabled: false } });
    assert.deepEqual(saveGlobalFastModeState(configPath, { enabled: true }), { success: true });
    assert.deepEqual(loadGlobalFastModeState(configPath), { state: { enabled: true } });
    assert.deepEqual(JSON.parse(readFileSync(configPath, "utf8")), { enabled: true });
    assert.equal(statSync(configPath).mode & 0o777, 0o600);
    assert.equal(statSync(join(agentDir, "extensions", "pi-extension-codex-fast-mode")).mode & 0o777, 0o700);
  } finally {
    rmSync(agentDir, { recursive: true, force: true });
  }
});

test("invalid or unreadable global config fails safely to off", () => {
  const directory = mkdtempSync(join(tmpdir(), "fast-mode-invalid-"));
  const configPath = join(directory, "config.json");
  try {
    writeFileSync(configPath, "{ not-json", "utf8");
    const invalid = loadGlobalFastModeState(configPath);
    assert.deepEqual(invalid.state, { enabled: false });
    assert.match(invalid.warning ?? "", /Could not read/u);

    writeFileSync(configPath, JSON.stringify({ enabled: "yes" }), "utf8");
    const wrongType = loadGlobalFastModeState(configPath);
    assert.deepEqual(wrongType.state, { enabled: false });
    assert.match(wrongType.warning ?? "", /expected/u);

    chmodSync(configPath, 0o600);
  } finally {
    rmSync(directory, { recursive: true, force: true });
  }
});

test("command grammar and completions are deterministic", () => {
  assert.equal(parseFastModeCommand(""), "toggle");
  assert.equal(parseFastModeCommand(" ON "), "on");
  assert.equal(parseFastModeCommand("off"), "off");
  assert.equal(parseFastModeCommand("status"), "status");
  assert.equal(parseFastModeCommand("on now"), "invalid");
  assert.deepEqual(fastModeArgumentCompletions("o"), [
    { value: "on", label: "on" },
    { value: "off", label: "off" },
  ]);
  assert.deepEqual(fastModeArgumentCompletions("status"), [{ value: "status", label: "status" }]);
});

test("a new session inherits the global default while a branch snapshot takes precedence", () => {
  const inherited = createHarness({ globalEnabled: true });
  const inheritedStart = inherited.handlers.get("session_start");
  assert.ok(inheritedStart);
  inheritedStart!({}, inherited.context as unknown as ExtensionContext);
  assert.deepEqual(inherited.statusUpdates.at(-1), { key: FAST_MODE_STATUS_KEY, value: "on" });

  const overridden = createHarness({
    globalEnabled: true,
    entries: [{ type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: false } }],
  });
  const overriddenStart = overridden.handlers.get("session_start");
  assert.ok(overriddenStart);
  overriddenStart!({}, overridden.context as unknown as ExtensionContext);
  assert.deepEqual(overridden.statusUpdates.at(-1), { key: FAST_MODE_STATUS_KEY, value: "off" });
});

test("extension restores state, publishes status, and persists session and global mutations", async () => {
  const harness = createHarness({
    entries: [{ type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: true } }],
  });
  const sessionStart = harness.handlers.get("session_start");
  const request = harness.handlers.get("before_provider_request");
  const command = harness.commands.get("fast-mode");
  assert.ok(sessionStart && request && command);

  sessionStart!({}, harness.context as unknown as ExtensionContext);
  assert.deepEqual(harness.statusUpdates.at(-1), { key: FAST_MODE_STATUS_KEY, value: "on" });
  assert.deepEqual(request!({ payload: { service_tier: "standard", keep: true } }, harness.context), {
    service_tier: FAST_MODE_SERVICE_TIER,
    keep: true,
  });

  await command.handler("off", harness.context);
  assert.deepEqual(harness.appendCalls, [{ customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: false } }]);
  assert.deepEqual(harness.saveCalls.map((call) => call.state), [{ enabled: false }]);
  assert.deepEqual(harness.statusUpdates.at(-1), { key: FAST_MODE_STATUS_KEY, value: "off" });
  assert.equal(request!({ payload: { keep: true } }, harness.context), undefined);

  await command.handler("status", harness.context);
  assert.match(harness.notifications.at(-1)?.message ?? "", /Fast mode: off.*New sessions default to off/u);
  assert.equal(harness.notifications.at(-1)?.level, "info");
});

test("an explicit no-op mode command still refreshes the global default", async () => {
  const harness = createHarness({ globalEnabled: true });
  const sessionStart = harness.handlers.get("session_start");
  const command = harness.commands.get("fast-mode");
  assert.ok(sessionStart && command);
  sessionStart!({}, harness.context as unknown as ExtensionContext);

  await command.handler("on", harness.context);
  assert.equal(harness.appendCalls.length, 0);
  assert.deepEqual(harness.saveCalls.map((call) => call.state), [{ enabled: true }]);
});

test("a global save failure leaves the session usable and reports the persistence failure", async () => {
  const harness = createHarness({ saveError: "read-only filesystem" });
  const sessionStart = harness.handlers.get("session_start");
  const command = harness.commands.get("fast-mode");
  assert.ok(sessionStart && command);
  sessionStart!({}, harness.context as unknown as ExtensionContext);

  await command.handler("on", harness.context);
  assert.deepEqual(harness.appendCalls, [{ customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: true } }]);
  assert.equal(harness.notifications.at(-1)?.level, "warning");
  assert.match(harness.notifications.at(-1)?.message ?? "", /could not be saved.*read-only filesystem/u);
});

test("extension rejects every mutation while busy but leaves status readable", async () => {
  const harness = createHarness({ busy: true });
  const sessionStart = harness.handlers.get("session_start");
  const command = harness.commands.get("fast-mode");
  assert.ok(sessionStart && command);
  sessionStart!({}, harness.context as unknown as ExtensionContext);

  await command.handler("on", harness.context);
  await command.handler("", harness.context);
  await command.handler("off", harness.context);
  assert.equal(harness.appendCalls.length, 0);
  assert.equal(harness.saveCalls.length, 0);
  assert.equal(harness.notifications.filter((item) => item.level === "warning").length, 3);

  await command.handler("status", harness.context);
  assert.match(harness.notifications.at(-1)?.message ?? "", /Fast mode: off/u);
  assert.equal(harness.notifications.at(-1)?.level, "info");

  harness.setBusy(false);
  harness.setPending(true);
  await command.handler("on", harness.context);
  assert.equal(harness.appendCalls.length, 0);
  assert.equal(harness.saveCalls.length, 0);
});

test("tree navigation reconstructs the active branch state", () => {
  const harness = createHarness({
    globalEnabled: true,
    entries: [{ type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: true } }],
  });
  const sessionStart = harness.handlers.get("session_start");
  const sessionTree = harness.handlers.get("session_tree");
  assert.ok(sessionStart && sessionTree);

  sessionStart!({}, harness.context as unknown as ExtensionContext);
  harness.entries.push({ type: "custom", customType: FAST_MODE_STATE_ENTRY_TYPE, data: { enabled: false } });
  sessionTree!({}, harness.context as unknown as ExtensionContext);
  assert.deepEqual(harness.statusUpdates.at(-1), { key: FAST_MODE_STATUS_KEY, value: "off" });
});
