import assert from "node:assert/strict";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";

import { loadConfig } from "./config.js";

test("loadConfig uses the explicit safe-runtime config path", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-config-"));
  const configPath = join(directory, "outside-runtime.json");
  const previous = process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
  await writeFile(configPath, JSON.stringify({
    codexCliPath: "codex.cmd",
    engine: "fake",
    bindHost: "127.0.0.1",
    port: 43110,
    dataDir: join(directory, "data"),
    accounts: [{ id: "main", label: "Main", codexHome: join(directory, "home") }],
    workspaces: [{ id: "project", label: "Project", path: directory }]
  }));

  process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = configPath;
  try {
    const result = await loadConfig();
    assert.equal(result.configPath, configPath);
    assert.equal(result.config.port, 43110);
  } finally {
    if (previous === undefined) delete process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
    else process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = previous;
    await rm(directory, { recursive: true, force: true });
  }
});

test("loadConfig accepts an empty accounts list for first-run recovery", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-config-"));
  const configPath = join(directory, "empty-accounts.json");
  const previous = process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
  await writeFile(configPath, JSON.stringify({
    codexCliPath: "codex.cmd",
    engine: "fake",
    bindHost: "127.0.0.1",
    port: 43110,
    dataDir: join(directory, "data"),
    accounts: [],
    workspaces: [{ id: "project", label: "Project", path: directory }]
  }));

  process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = configPath;
  try {
    const result = await loadConfig();
    assert.deepEqual(result.config.accounts, []);
    assert.equal(result.configPath, configPath);
  } finally {
    if (previous === undefined) delete process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
    else process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = previous;
    await rm(directory, { recursive: true, force: true });
  }
});

test("loadConfig accepts the current versioned configuration", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-config-"));
  const configPath = join(directory, "versioned.json");
  const previous = process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
  await writeFile(configPath, JSON.stringify({
    schemaVersion: 1,
    codexCliPath: "codex.cmd",
    engine: "fake",
    bindHost: "127.0.0.1",
    port: 43110,
    dataDir: join(directory, "data"),
    accounts: [],
    workspaces: [{ id: "project", label: "Project", path: directory }]
  }));

  process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = configPath;
  try {
    const result = await loadConfig();
    assert.equal(result.config.schemaVersion, 1);
  } finally {
    if (previous === undefined) delete process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
    else process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = previous;
    await rm(directory, { recursive: true, force: true });
  }
});

test("loadConfig accepts only literal IPv4 and IPv6 loopback bind hosts", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-config-"));
  const configPath = join(directory, "bind-host.json");
  const previous = process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
  const base = {
    schemaVersion: 1,
    codexCliPath: "codex.cmd",
    engine: "fake",
    port: 43110,
    dataDir: join(directory, "data"),
    accounts: [],
    workspaces: [{ id: "project", label: "Project", path: directory }]
  };
  process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = configPath;
  try {
    for (const bindHost of ["127.0.0.1", "127.42.0.9", "::1", "0:0:0:0:0:0:0:1"]) {
      await writeFile(configPath, JSON.stringify({ ...base, bindHost }));
      assert.equal((await loadConfig()).config.bindHost, bindHost);
    }

    for (const bindHost of ["0.0.0.0", "::", "192.168.1.20", "localhost"]) {
      await writeFile(configPath, JSON.stringify({ ...base, bindHost }));
      await assert.rejects(loadConfig(), (error) => error.message === "configuration_bind_host_not_loopback");
    }
  } finally {
    if (previous === undefined) delete process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
    else process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = previous;
    await rm(directory, { recursive: true, force: true });
  }
});

test("loadConfig validates an optional local permission ceiling", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-config-"));
  const configPath = join(directory, "permission-ceiling.json");
  const previous = process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
  const base = {
    schemaVersion: 1,
    codexCliPath: "codex.cmd",
    engine: "fake",
    bindHost: "127.0.0.1",
    port: 43110,
    dataDir: join(directory, "data"),
    accounts: [],
    workspaces: [{ id: "project", label: "Project", path: directory }]
  };
  process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = configPath;
  try {
    await writeFile(configPath, JSON.stringify({
      ...base,
      permissionCeiling: { permissionProfile: ":danger-full-access", approvalPolicy: "never" }
    }));
    assert.deepEqual((await loadConfig()).config.permissionCeiling, {
      permissionProfile: ":danger-full-access",
      approvalPolicy: "never"
    });

    await writeFile(configPath, JSON.stringify({
      ...base,
      permissionCeiling: { permissionProfile: ":browser-invented", approvalPolicy: "never" }
    }));
    await assert.rejects(loadConfig(), /configuration_invalid/);
  } finally {
    if (previous === undefined) delete process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
    else process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = previous;
    await rm(directory, { recursive: true, force: true });
  }
});

test("loadConfig accepts only an explicit boolean device authentication requirement", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-config-"));
  const configPath = join(directory, "device-authentication.json");
  const previous = process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
  const base = {
    schemaVersion: 1,
    codexCliPath: "codex.cmd",
    engine: "fake",
    bindHost: "127.0.0.1",
    port: 43110,
    dataDir: join(directory, "data"),
    accounts: [],
    workspaces: [{ id: "project", label: "Project", path: directory }]
  };
  process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = configPath;
  try {
    await writeFile(configPath, JSON.stringify({
      ...base,
      deviceAuthentication: { required: false }
    }));
    assert.deepEqual((await loadConfig()).config.deviceAuthentication, { required: false });

    for (const deviceAuthentication of [false, {}, { required: "false" }]) {
      await writeFile(configPath, JSON.stringify({ ...base, deviceAuthentication }));
      await assert.rejects(loadConfig(), /configuration_invalid/);
    }
  } finally {
    if (previous === undefined) delete process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
    else process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = previous;
    await rm(directory, { recursive: true, force: true });
  }
});

test("loadConfig rejects corrupt and unsupported schemas without exposing config details", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-config-"));
  const configPath = join(directory, "unsafe.json");
  const previous = process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
  process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = configPath;
  try {
    await writeFile(configPath, "{ private-setting");
    await assert.rejects(loadConfig(), (error) =>
      error.message === "configuration_json_invalid" && !error.message.includes(configPath));

    await writeFile(configPath, JSON.stringify({
      schemaVersion: 2,
      codexCliPath: "codex.cmd",
      engine: "fake",
      bindHost: "127.0.0.1",
      port: 43110,
      dataDir: join(directory, "data"),
      accounts: [],
      workspaces: [{ id: "project", label: "Project", path: directory }]
    }));
    await assert.rejects(loadConfig(), (error) =>
      error.message === "configuration_schema_unsupported" && !error.message.includes(configPath));
  } finally {
    if (previous === undefined) delete process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
    else process.env.CODE_REMOTE_MANAGER_CONFIG_PATH = previous;
    await rm(directory, { recursive: true, force: true });
  }
});
