import { test } from "node:test";
import assert from "node:assert/strict";
import { DatabaseSync } from "node:sqlite";
import {
  getApprovalsControlChat,
  setApprovalsControlChat,
} from "./inchatApprovals.ts";
import { ensureSchema as ensureWorkSchema, workDecisionPropose } from "./work.ts";
import { storeConversations, webConvId, type WebConversation } from "./sync/pull.ts";

function freshDb(): DatabaseSync {
  const db = new DatabaseSync(":memory:");
  db.exec("PRAGMA foreign_keys=ON;");
  db.exec(`CREATE TABLE unified_conversation (
    id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, channel TEXT NOT NULL, account_id TEXT,
    external_id TEXT, title TEXT, contact_id TEXT, last_ts INTEGER, unread INTEGER NOT NULL DEFAULT 0,
    archived_at INTEGER, muted INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL, deleted_at INTEGER);`);
  db.exec(`CREATE TABLE unified_message (
    id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, conversation_id TEXT NOT NULL, direction TEXT NOT NULL,
    body TEXT, sender TEXT, ts INTEGER NOT NULL, provider_msg_id TEXT, attachments TEXT, media_type TEXT,
    media_path TEXT, reaction TEXT, source TEXT, updated_at INTEGER NOT NULL, deleted_at INTEGER,
    FOREIGN KEY(conversation_id) REFERENCES unified_conversation(id));`);
  db.exec(`CREATE UNIQUE INDEX ux_msg_provider ON unified_message(provider_msg_id) WHERE provider_msg_id IS NOT NULL;`);
  ensureWorkSchema(db);
  return db;
}

function propose(db: DatabaseSync, title: string) {
  return workDecisionPropose(db, { module: "ops", title, proposal: "Needs Roki approval", risk: "external" }) as any;
}

function pullMessage(db: DatabaseSync, externalId: string, body: string, opts: { id?: string; direction?: "IN" | "OUT" } = {}) {
  const conv: WebConversation = {
    externalId,
    channel: "LINKEDIN",
    accountId: "int1",
    title: externalId,
    lastTs: 1000,
    unread: 1,
    messages: [{
      providerMsgId: opts.id ?? `${externalId}:m1`,
      direction: opts.direction ?? "IN",
      body,
      sender: "roki-phone",
      ts: 1000,
    }],
  };
  return storeConversations(db, [conv]);
}

test("approvals_control_chat setting defaults inert and can be set", () => {
  const db = freshDb();
  assert.deepEqual(getApprovalsControlChat(db), { control_chat_id: "", enabled: false, updated_at: null });

  const control = webConvId("LINKEDIN", "control");
  const set = setApprovalsControlChat(db, control);
  assert.equal(set.control_chat_id, control);
  assert.equal(set.enabled, true);
  assert.equal(getApprovalsControlChat(db).control_chat_id, control);
});

test("non-control chat inbound command is ignored", () => {
  const db = freshDb();
  const decision = propose(db, "Approve campaign send");
  setApprovalsControlChat(db, webConvId("LINKEDIN", "control"));

  pullMessage(db, "other", `approve ${decision.id}`);

  const row = db.prepare(`SELECT status, decided_by FROM work_decision WHERE id=?`).get(decision.id) as any;
  assert.equal(row.status, "proposed");
  assert.equal(row.decided_by, "");
  assert.equal((db.prepare(`SELECT COUNT(*) AS c FROM work_message`).get() as any).c, 0);
});

test("ambiguous decision prefix no-ops and logs the reason", () => {
  const db = freshDb();
  const d1 = propose(db, "Approve A");
  const d2 = propose(db, "Approve B");
  const prefix = "dec:ambiguous";
  db.prepare(`UPDATE work_decision SET id=? WHERE id=?`).run(`${prefix}:one`, d1.id);
  db.prepare(`UPDATE work_decision SET id=? WHERE id=?`).run(`${prefix}:two`, d2.id);
  setApprovalsControlChat(db, webConvId("LINKEDIN", "control"));

  pullMessage(db, "control", `approve ${prefix}`);

  const rows = db.prepare(`SELECT id, status, decided_by FROM work_decision ORDER BY id`).all() as any[];
  assert.deepEqual(rows.map((row) => [row.id, row.status, row.decided_by]), [
    [`${prefix}:one`, "proposed", ""],
    [`${prefix}:two`, "proposed", ""],
  ]);
  const log = db.prepare(`SELECT body, metadata_json FROM work_message WHERE kind='approval_chat_result'`).get() as any;
  assert.match(log.body, /ambiguous decision prefix/);
  assert.equal(JSON.parse(log.metadata_json).reason, "ambiguous decision prefix");
});

test("approve command transitions a pending decision through the direct approval path", () => {
  const db = freshDb();
  const decision = propose(db, "Approve external send");
  const control = webConvId("LINKEDIN", "control");
  setApprovalsControlChat(db, control);

  pullMessage(db, "control", `approve ${decision.id.slice(0, -2)}`);

  const row = db.prepare(`SELECT status, decided_by, rationale FROM work_decision WHERE id=?`).get(decision.id) as any;
  assert.equal(row.status, "approved");
  assert.equal(row.decided_by, `chat:${control}`);
  assert.equal(row.rationale, "");

  const log = db.prepare(`SELECT status, decision_id, reason FROM approval_chat_command_log`).get() as any;
  assert.equal(log.status, "approved");
  assert.equal(log.decision_id, decision.id);
  assert.equal(log.reason, "");
});

test("deny command rejects and stores the reason", () => {
  const db = freshDb();
  const decision = propose(db, "Deny risky send");
  const control = webConvId("LINKEDIN", "control");
  setApprovalsControlChat(db, control);

  pullMessage(db, "control", `deny ${decision.id} wrong recipient`);

  const row = db.prepare(`SELECT status, decided_by, rationale FROM work_decision WHERE id=?`).get(decision.id) as any;
  assert.equal(row.status, "rejected");
  assert.equal(row.decided_by, `chat:${control}`);
  assert.equal(row.rationale, "wrong recipient");
});

test("forwarded or quoted multiline approve text is ignored", () => {
  const db = freshDb();
  const decision = propose(db, "Approve external send");
  setApprovalsControlChat(db, webConvId("LINKEDIN", "control"));

  pullMessage(db, "control", `Forwarded from Ops\napprove ${decision.id}`);

  const row = db.prepare(`SELECT status, decided_by FROM work_decision WHERE id=?`).get(decision.id) as any;
  assert.equal(row.status, "proposed");
  assert.equal(row.decided_by, "");
  assert.equal((db.prepare(`SELECT COUNT(*) AS c FROM approval_chat_command_log`).get() as any).c, 0);
});

test("replay ledger is keyed by conversation and provider message id, not command content", () => {
  const db = freshDb();
  const d1 = propose(db, "Approve first send");
  const d2 = propose(db, "Approve second send");
  const control = webConvId("LINKEDIN", "control");
  setApprovalsControlChat(db, control);

  pullMessage(db, "control", `approve ${d1.id}`, { id: "provider-msg-1" });
  pullMessage(db, "control", `approve ${d2.id}`, { id: "provider-msg-2" });
  pullMessage(db, "control", `approve ${d2.id}`, { id: "provider-msg-2" });

  const rows = db.prepare(`SELECT id, status FROM work_decision ORDER BY created_at`).all() as any[];
  assert.deepEqual(rows.map((row) => [row.id, row.status]), [
    [d1.id, "approved"],
    [d2.id, "approved"],
  ]);
  assert.equal((db.prepare(`SELECT COUNT(*) AS c FROM approval_chat_command_log`).get() as any).c, 2);
});

test("chat id collision across channels cannot redirect approval control", () => {
  const db = freshDb();
  const decision = propose(db, "Approve channel-scoped send");
  setApprovalsControlChat(db, webConvId("LINKEDIN", "control"));

  storeConversations(db, [{
    externalId: "control",
    channel: "TELEGRAM",
    accountId: "tg1",
    title: "control",
    lastTs: 1000,
    unread: 1,
    messages: [{ providerMsgId: "tg-control:m1", direction: "IN", body: `approve ${decision.id}`, sender: "sam", ts: 1000 }],
  }]);

  const row = db.prepare(`SELECT status, decided_by FROM work_decision WHERE id=?`).get(decision.id) as any;
  assert.equal(row.status, "proposed");
  assert.equal(row.decided_by, "");
});

test("deny without reason no-ops and logs instead of crashing", () => {
  const db = freshDb();
  const decision = propose(db, "Deny risky send");
  setApprovalsControlChat(db, webConvId("LINKEDIN", "control"));

  pullMessage(db, "control", `deny ${decision.id}`);

  const row = db.prepare(`SELECT status, decided_by, rationale FROM work_decision WHERE id=?`).get(decision.id) as any;
  assert.equal(row.status, "proposed");
  assert.equal(row.decided_by, "");
  assert.equal(row.rationale, "");
  const log = db.prepare(`SELECT status, reason FROM approval_chat_command_log`).get() as any;
  assert.equal(log.status, "ignored");
  assert.equal(log.reason, "deny requires a reason");
});
