const assert = require("assert");
const fs = require("fs");
const path = require("path");
const {
  generateCommandPreview,
  findDynamicSegments,
  normalizePreviewText,
  previewParts
} = require("../src/services/command-preview");
const { runAdvancedCommand } = require("../src/services/commands");
const {
  conditionMatches,
  selectRandomReply,
  validateRandomConfig
} = require("../src/services/command-random");
const {
  DELAY_MS,
  issueConfirmation,
  consumeConfirmation,
  isDestructivePath
} = require("../src/services/destructive-confirm");
const { db, migrate } = require("../src/services/db");

async function run() {
  migrate();
  const columns = db.prepare("PRAGMA table_info(custom_commands)").all().map((row) => row.name);
  for (const column of [
    "preview_text",
    "preview_status",
    "preview_error",
    "preview_generated_at",
    "preview_dynamic_segments",
    "description",
    "random_replies_json",
    "rng_enabled",
    "rng_min",
    "rng_max"
  ]) {
    assert(columns.includes(column), `Missing custom command preview column: ${column}`);
  }

  db.exec("SAVEPOINT verify_command_description");
  try {
    const now = Date.now();
    const trigger = `verify-description-${now}`;
    const inserted = db.prepare(
      "INSERT INTO custom_commands (trigger, description, response, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
    ).run(trigger, "Initial description", "Test reply", now, now);
    db.prepare("UPDATE custom_commands SET description = ? WHERE id = ?")
      .run("Updated description", inserted.lastInsertRowid);
    assert.equal(
      db.prepare("SELECT description FROM custom_commands WHERE id = ?")
        .get(inserted.lastInsertRowid).description,
      "Updated description"
    );
  } finally {
    db.exec("ROLLBACK TO verify_command_description; RELEASE verify_command_description");
  }

  const preview = await generateCommandPreview({
    language: "js",
    code: `
      function run(ctx) {
        ctx.reply("Hello " + ctx.user.username);
        return "Balance 1234, id " + ctx.user.id + ", date " + new Date().toISOString();
      }
    `
  });
  assert.equal(preview.preview_status, "ready");
  assert.match(preview.preview_text, /Hello some_user/);
  assert.equal(preview.preview_text.includes("Some User"), false);
  const segments = JSON.parse(preview.preview_dynamic_segments);
  for (const type of ["username", "number", "id", "date"]) {
    assert(segments.some((segment) => segment.type === type), `Missing ${type} dynamic segment`);
  }
  assert(previewParts(preview.preview_text, segments).some((part) => part.dynamic));
  assert.equal(normalizePreviewText("Some User and some-user"), "some_user and some_user");
  assert(findDynamicSegments("some_user 42 user_123 2026-01-02T12:34:56.000Z").length, 4);
  const punctuationPreview = "Fun fact for some_user:\nExample";
  assert.equal(
    previewParts(punctuationPreview, findDynamicSegments(punctuationPreview)).map((part) => part.text).join(""),
    punctuationPreview
  );

  const snippetPreview = await generateCommandPreview({
    language: "js",
    code: 'return "Snippet for " + ctx.user.username;'
  });
  assert.equal(snippetPreview.preview_status, "ready");
  assert.equal(snippetPreview.preview_text, "Snippet for some_user");

  const exportedPreview = await generateCommandPreview({
    language: "js",
    code: 'module.exports = (ctx) => "Export for " + ctx.user.username;'
  });
  assert.equal(exportedPreview.preview_status, "ready");
  assert.equal(exportedPreview.preview_text, "Export for some_user");

  const safeBuiltInsPreview = await generateCommandPreview({
    language: "js",
    code: `
      const facts = ["using a process called respiration"];
      const selected = facts[Math.floor(Math.random() * facts.length)];
      const url = new URL("https://example.com/facts?count=1");
      return JSON.stringify({ selected, host: url.hostname, count: url.searchParams.get("count") });
    `
  });
  assert.equal(safeBuiltInsPreview.preview_status, "ready");
  assert.deepEqual(JSON.parse(safeBuiltInsPreview.preview_text), {
    selected: "using a process called respiration",
    host: "example.com",
    count: "1"
  });

  for (const code of [
    'return Math.constructor.constructor("return pro" + "cess")().cwd();',
    'return ctx.constructor.constructor("return pro" + "cess")().cwd();',
    'return URL.constructor.constructor("return pro" + "cess")().cwd();'
  ]) {
    const constructorEscape = await generateCommandPreview({ language: "js", code });
    assert.equal(constructorEscape.preview_status, "unavailable");
    assert.match(constructorEscape.preview_error, /code generation from strings disallowed/i);
  }

  const pythonSnippetPreview = await generateCommandPreview({
    language: "python",
    code: 'return "Python snippet"'
  });
  assert.equal(pythonSnippetPreview.preview_status, "ready");
  assert.equal(pythonSnippetPreview.preview_text, "Python snippet");

  assert.equal(await runAdvancedCommand({ language: "js", code: 'return "wrapped";' }, {}), "wrapped");

  const randomConfig = {
    replies: [
      { text: "Low {{core.command.rng}}", weight: 1, condition: "<20" },
      { text: "High", weight: 2, condition: ">=20" }
    ],
    rngEnabled: true,
    rngMin: 1,
    rngMax: 100
  };
  const rolls = [0.1, 0.9];
  const selectedRandom = selectRandomReply(randomConfig, () => rolls.shift());
  assert.equal(selectedRandom.rng, 11);
  assert.equal(selectedRandom.reply.text, "Low {{core.command.rng}}");
  assert.equal(conditionMatches(">=10", 10), true);
  assert.equal(conditionMatches("<20", 20), false);
  assert.equal(validateRandomConfig({ replies: [{ text: "Hi", weight: 1000 }], rngMin: 1, rngMax: 100 }).ok, false);
  assert.equal(validateRandomConfig({ replies: [{ text: "Hi", weight: 1, condition: "maybe" }], rngEnabled: true }).ok, false);

  const noSideEffects = await generateCommandPreview({
    language: "js",
    code: `
      async function run(ctx) {
        await ctx.economy.add(ctx.user.id, 999999);
        await ctx.inventory.remove(ctx.user.id, "all");
        await ctx.moderation.ban(ctx.user.id);
        await ctx.files.write("outside.txt", "blocked");
        ctx.db.prepare("DELETE FROM users").run();
        return "Balance " + await ctx.economy.getBalance(ctx.user.id);
      }
    `
  });
  assert.equal(noSideEffects.preview_status, "ready");
  assert.equal(noSideEffects.preview_text, "Balance 1234");

  const readOnlyFetch = await generateCommandPreview({
    language: "js",
    code: `
      async function run() {
        const response = await fetch("data:application/json,%7B%22message%22%3A%22preview%22%7D");
        const data = await response.json();
        return data.message;
      }
    `
  });
  assert.equal(readOnlyFetch.preview_status, "ready");
  assert.equal(readOnlyFetch.preview_text, "preview");

  const blockedNetworkFetch = await generateCommandPreview({
    language: "js",
    code: `
      async function run() {
        await fetch("http://127.0.0.1:3000/admin/settings");
        return "no";
      }
    `
  });
  assert.equal(blockedNetworkFetch.preview_status, "unavailable");
  assert.match(blockedNetworkFetch.preview_error, /does not send external network requests/i);

  const blockedWriteFetch = await generateCommandPreview({
    language: "js",
    code: `
      async function run() {
        await fetch("data:text/plain,no", { method: "POST", body: "write" });
        return "no";
      }
    `
  });
  assert.equal(blockedWriteFetch.preview_status, "unavailable");
  assert.match(blockedWriteFetch.preview_error, /read-only GET and HEAD/i);

  const blocked = await generateCommandPreview({
    language: "js",
    code: "function run() { return process.cwd(); }"
  });
  assert.equal(blocked.preview_status, "unavailable");
  assert.match(blocked.preview_error, /blocks filesystem, process/i);

  const blockedImport = await generateCommandPreview({
    language: "python",
    code: "import os\ndef run(ctx):\n    return os.getcwd()"
  });
  assert.equal(blockedImport.preview_status, "unavailable");
  assert.match(blockedImport.preview_error, /blocks imports/i);

  const timedOut = await generateCommandPreview({
    language: "js",
    code: "function run() { while (true) {} }"
  });
  assert.equal(timedOut.preview_status, "unavailable");
  assert.match(timedOut.preview_error, /timed out/i);

  const req = fakeRequest();
  assert.equal(isDestructivePath("/admin/commands/1/delete"), true);
  assert.equal(isDestructivePath("/admin/commands/1/update"), false);
  const first = issueConfirmation(req, "/admin/commands/1/delete");
  assert.equal(first.delay_seconds, DELAY_MS / 1000);
  assert.equal(consumeConfirmation(req, "/admin/commands/1/delete", first.token).reason, "too_early");

  const second = issueConfirmation(req, "/admin/commands/1/delete");
  req.session.destructive_confirmations[second.token].not_before = Date.now() - 1;
  assert.equal(consumeConfirmation(req, "/admin/pages/1/delete", second.token).reason, "action_mismatch");

  const third = issueConfirmation(req, "/admin/commands/1/delete");
  req.session.destructive_confirmations[third.token].not_before = Date.now() - 1;
  assert.equal(consumeConfirmation(req, "/admin/commands/1/delete", third.token).valid, true);
  assert.equal(consumeConfirmation(req, "/admin/commands/1/delete", third.token).valid, false);

  const appScript = fs.readFileSync(path.join(__dirname, "..", "src", "web", "public", "app.js"), "utf8");
  const layout = fs.readFileSync(path.join(__dirname, "..", "src", "web", "views", "partials", "layout-bottom.ejs"), "utf8");
  const commandView = fs.readFileSync(path.join(__dirname, "..", "src", "web", "views", "admin-commands.ejs"), "utf8");
  const publicCommandView = fs.readFileSync(path.join(__dirname, "..", "src", "web", "views", "commands.ejs"), "utf8");
  const publicCommandContent = fs.readFileSync(path.join(__dirname, "..", "src", "web", "views", "partials", "command-list-content.ejs"), "utf8");
  assert(appScript.includes("${confirmLabel(form, submitter)} in ${remaining}"));
  assert(appScript.includes('button.disabled = remaining > 0'));
  assert(appScript.includes('fetch("/api/destructive-confirmations"'));
  assert(appScript.includes("destructiveToken: timedDestructiveToken"));
  assert(appScript.includes("destructiveFetch(action"));
  assert(appScript.includes('headers.set("X-Confirmation-Token", token)'));
  assert(appScript.includes("event.preventDefault();"));
  assert(appScript.includes("expiryTimer"));
  assert(appScript.includes("resetDestructive"));
  assert(layout.includes("data-destructive-modal"));
  assert(layout.includes("data-destructive-confirm disabled"));
  assert(commandView.includes("Preview unavailable"));
  assert(commandView.includes("preview-dynamic"));
  assert(commandView.includes('data-collapsed-label="Read more"'));
  assert(commandView.includes('data-expanded-label="Read less"'));
  assert.equal(commandView.includes("Full preview"), false);
  assert.equal(commandView.includes("command-preview-full"), false);
  assert(appScript.includes('[data-expand-text]'));
  assert(commandView.includes(">Static<"));
  assert(commandView.includes(">Random Reply<"));
  assert(commandView.includes(">Dynamic<"));
  assert(commandView.includes("random_weight"));
  assert(commandView.includes("{{core.command.rng}}"));
  assert(commandView.includes('name="description"'));
  assert(publicCommandView.includes('data-command-content-toggle'));
  assert(publicCommandView.includes('partials/command-list-content'));
  assert(publicCommandContent.includes("command.preview.parts"));
  assert(publicCommandContent.includes("data-command-content-description"));
  assert.equal(publicCommandContent.includes("Refresh preview"), false);
  assert(appScript.includes('row.style.display = expanded ? "table-row" : "none"'));
  assert(appScript.includes("[data-command-list-content]"));
  assert.equal(commandView.includes('"Advanced (" + command.language + ")"'), false);

  console.log("Command preview and destructive confirmation verification passed.");
}

function fakeRequest() {
  return {
    session: {
      user: { id: "admin-user", isAdmin: true }
    },
    body: {},
    get() {
      return null;
    }
  };
}

run().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
