// Assertions for npm onboard channel-agent E2E scenarios.
import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import {
  assertAgentReplyContainsMarker,
  assertOpenAiRequestLogUsed,
} from "../agent-turn-output.mjs";
import { assertOpenAiEnvAuthProfileStore } from "../auth-profile-store-assertions.mjs";
import { readPositiveIntEnv } from "../env-limits.mjs";
import {
  applyMockOpenAiModelConfig,
  parseMockOpenAiPort,
} from "../fixtures/mock-openai-config.mjs";
import { readTextFileBounded, readTextFileTail } from "../text-file-utils.mjs";

const command = process.argv[2];
const ERROR_DETAIL_TAIL_BYTES = 16 * 1024;
const JSON_ARTIFACT_MAX_BYTES = readPositiveIntEnv(
  "OPENCLAW_NPM_ONBOARD_JSON_ARTIFACT_MAX_BYTES",
  1024 * 1024,
);
const STATUS_TEXT_MAX_BYTES = readPositiveIntEnv(
  "OPENCLAW_NPM_ONBOARD_STATUS_TEXT_MAX_BYTES",
  1024 * 1024,
);
const ansiEscapePattern = new RegExp(String.raw`\u001b\[[0-?]*[ -/]*[@-~]`, "g");

function readJson(file) {
  return JSON.parse(
    readTextFileBounded(file, "JSON artifact", JSON_ARTIFACT_MAX_BYTES, {
      tailBytes: ERROR_DETAIL_TAIL_BYTES,
    }),
  );
}

function stripAnsi(text) {
  return text.replace(ansiEscapePattern, "");
}

const statusSectionTitles = new Set([
  "openclaw status",
  "overview",
  "plugin compatibility",
  "model selection",
  "security audit",
  "channels",
  "sessions",
  "system events",
  "health",
  "usage",
]);

function normalizedStatusHeading(line) {
  return stripAnsi(line)
    .trim()
    .replace(/^#+\s*/, "")
    .trim()
    .toLowerCase();
}

function extractStatusSection(text, title) {
  const target = title.toLowerCase();
  const lines = text.split(/\r?\n/);
  const start = lines.findIndex((line) => normalizedStatusHeading(line) === target);
  if (start === -1) {
    return null;
  }
  const section = [];
  for (const line of lines.slice(start + 1)) {
    const normalized = normalizedStatusHeading(line);
    if (normalized && statusSectionTitles.has(normalized)) {
      break;
    }
    section.push(line);
  }
  return stripAnsi(section.join("\n"));
}

function readAuthProfileStoreText(agentDir) {
  const dbPath = path.join(agentDir, "openclaw-agent.sqlite");
  if (!fs.existsSync(dbPath)) {
    return "";
  }
  let db;
  try {
    db = new DatabaseSync(dbPath, { readOnly: true });
    const row = db
      .prepare("SELECT store_json FROM auth_profile_store WHERE store_key = ?")
      .get("primary");
    return typeof row?.store_json === "string" ? row.store_json : "";
  } catch {
    return "";
  } finally {
    db?.close();
  }
}

function assertOnboardState() {
  const home = process.argv[3];
  const stateDir = path.join(home, ".openclaw");
  const configPath = path.join(stateDir, "openclaw.json");
  const agentDir = path.join(stateDir, "agents", "main", "agent");

  if (!fs.existsSync(configPath)) {
    throw new Error("onboard did not write openclaw.json");
  }
  if (!fs.existsSync(agentDir)) {
    throw new Error("onboard did not create main agent dir");
  }
  const authStoreText = readAuthProfileStoreText(agentDir);
  if (!authStoreText) {
    throw new Error("onboard did not persist auth profile store");
  }
  assertOpenAiEnvAuthProfileStore(authStoreText, {
    envRefMessage: "auth profile did not persist OPENAI_API_KEY env ref",
    rawKeyMessage: "auth profile persisted the raw OpenAI test key",
    rawKeyNeedle: "sk-openclaw-npm-onboard-e2e",
  });
}

function configureMockModel() {
  const mockPort = parseMockOpenAiPort(process.argv[3]);
  const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
  const cfg = readJson(configPath);
  applyMockOpenAiModelConfig(cfg, { mockPort });
  fs.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\n`);
}

function assertMockModelConfig() {
  const mockPort = parseMockOpenAiPort(process.argv[3]);
  const expectedModelRef = "openai/gpt-5.5";
  const expectedBaseUrl = `http://127.0.0.1:${mockPort}/v1`;
  const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
  const cfg = readJson(configPath);
  const provider = cfg.models?.providers?.openai;
  const defaultModel = cfg.agents?.defaults?.model?.primary;
  const defaultRuntime = cfg.agents?.defaults?.models?.[expectedModelRef]?.agentRuntime?.id;
  const agent = Array.isArray(cfg.agents?.list)
    ? (cfg.agents.list.find((entry) => entry?.id === "main") ?? cfg.agents.list[0])
    : undefined;
  const agentModel = agent?.model?.primary;
  const agentRuntime = agent?.models?.[expectedModelRef]?.agentRuntime?.id;
  if (provider?.baseUrl !== expectedBaseUrl) {
    throw new Error(
      `mock OpenAI baseUrl was not preserved; expected ${expectedBaseUrl}, got ${provider?.baseUrl}`,
    );
  }
  if (provider?.api !== "openai-responses") {
    throw new Error(`mock OpenAI api was not preserved; got ${provider?.api}`);
  }
  if (provider?.agentRuntime?.id !== "openclaw") {
    throw new Error(`mock OpenAI runtime was not preserved; got ${provider?.agentRuntime?.id}`);
  }
  if (defaultModel !== expectedModelRef) {
    throw new Error(
      `mock default model was not preserved; expected ${expectedModelRef}, got ${defaultModel}`,
    );
  }
  if (defaultRuntime !== "openclaw") {
    throw new Error(`mock default runtime was not preserved; got ${defaultRuntime}`);
  }
  if (agent && agentModel !== expectedModelRef) {
    throw new Error(
      `mock agent model was not preserved; expected ${expectedModelRef}, got ${agentModel}`,
    );
  }
  if (agent && agentRuntime !== "openclaw") {
    throw new Error(`mock agent runtime was not preserved; got ${agentRuntime}`);
  }
}

function assertChannelConfig() {
  const channel = process.argv[3];
  const expectedTokens = process.argv.slice(4);
  const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
  const cfg = readJson(configPath);
  const entry = cfg.channels?.[channel];
  if (!entry || entry.enabled === false) {
    throw new Error(`${channel} was not enabled`);
  }
  const assertTokenField = (field, expected) => {
    if (entry[field] !== expected) {
      throw new Error(
        `${channel} config did not persist ${field}; expected ${expected}, got ${JSON.stringify(entry[field])}`,
      );
    }
  };
  switch (channel) {
    case "telegram": {
      if (expectedTokens.length !== 1) {
        throw new Error("telegram channel config assertion requires one bot token");
      }
      assertTokenField("botToken", expectedTokens[0]);
      return;
    }
    case "discord": {
      if (expectedTokens.length !== 1) {
        throw new Error("discord channel config assertion requires one bot token");
      }
      assertTokenField("token", expectedTokens[0]);
      return;
    }
    case "slack": {
      if (expectedTokens.length !== 2) {
        throw new Error("slack channel config assertion requires bot and app tokens");
      }
      assertTokenField("botToken", expectedTokens[0]);
      assertTokenField("appToken", expectedTokens[1]);
      return;
    }
    default:
      throw new Error(`unsupported channel config assertion: ${channel}`);
  }
}

function assertStatusSurfaces() {
  const channel = process.argv[3];
  const channelsStatusPath = process.argv[4];
  const statusTextPath = process.argv[5];
  const channelsStatus = readJson(channelsStatusPath);
  const statusText = readTextFileBounded(
    statusTextPath,
    "plain status output",
    STATUS_TEXT_MAX_BYTES,
    { tailBytes: ERROR_DETAIL_TAIL_BYTES },
  );
  const statusTail = readTextFileTail(statusTextPath, ERROR_DETAIL_TAIL_BYTES);
  const configuredChannels = Array.isArray(channelsStatus.configuredChannels)
    ? channelsStatus.configuredChannels
    : [];
  if (!configuredChannels.includes(channel)) {
    throw new Error(
      `channels status did not list configured channel ${channel}. Payload: ${JSON.stringify(channelsStatus)}`,
    );
  }
  if (!/channels/i.test(statusText)) {
    throw new Error(
      `plain status output did not render a Channels section. Output tail: ${statusTail}`,
    );
  }
  const channelsSection = extractStatusSection(statusText, "channels");
  if (!channelsSection) {
    throw new Error(
      `plain status output did not render a Channels section. Output tail: ${statusTail}`,
    );
  }
  if (!channelsSection.toLowerCase().includes(channel.toLowerCase())) {
    throw new Error(
      `plain status output did not mention ${channel} in the Channels section. Output tail: ${statusTail}`,
    );
  }
}

function assertAgentTurn() {
  const marker = process.argv[3];
  const logPath = process.argv[4];
  assertAgentReplyContainsMarker(marker, "/tmp/openclaw-agent.combined");
  assertOpenAiRequestLogUsed(logPath);
}

const commands = {
  "assert-onboard-state": assertOnboardState,
  "configure-mock-model": configureMockModel,
  "assert-mock-model-config": assertMockModelConfig,
  "assert-channel-config": assertChannelConfig,
  "assert-status-surfaces": assertStatusSurfaces,
  "assert-agent-turn": assertAgentTurn,
};

const fn = commands[command];
if (!fn) {
  throw new Error(`unknown npm onboard/channel/agent assertion command: ${command}`);
}
fn();
