/**
 * The write tools, up to but not through the transaction.
 *
 * What these cases pin is the promise the approval flow makes: that a change
 * the user has not agreed to leaves the database exactly as it was. So the
 * database here is a fake whose only real job is to COUNT how many transactions
 * were opened, because "nothing was written" is not a claim about a row — it is
 * a claim that no write was ever attempted, and a row check would pass just as
 * happily against a write that failed for some other reason.
 *
 * `e2e/write-tools.test.ts` takes the applied path through a real Postgres and
 * reads the audit row back. This file deliberately never reaches one: the unit
 * suite runs in CI before the migration step, against a database with no
 * tables.
 */
import assert from 'node:assert/strict';
import test from 'node:test';
import type { AgentToolResult, ExtensionContext } from '@earendil-works/pi-coding-agent';
import {
  PIGGY_ALWAYS_CONFIRM_KINDS,
  isGuardedKind,
  requiresApproval,
  type PiggyApprovalDecision,
  type PiggyProposedChange,
} from '@pig/core';
import type { Principal } from '@pig/api/src/lib/auth';
import type { Database } from '@pig/db';
import { getTableName, type Table } from 'drizzle-orm';
import { createPigWriteTools, type PigWriteDetails } from '../src/write-tools';

const ctx = {} as ExtensionContext;

const ACCOUNT_ID = '11111111-1111-4111-8111-111111111111';
const DEAL_ID = '22222222-2222-4222-8222-222222222222';

/** A member of both pipelines: the ordinary GTM user, not an admin. */
function seller(overrides: Partial<Principal> = {}): Principal {
  return {
    userId: '33333333-3333-4333-8333-333333333333',
    email: 'dana@primeintellect.ai',
    name: 'Dana Okonjo',
    isPlatformAdmin: false,
    teams: [
      { team: 'demand', role: 'member' },
      { team: 'supply', role: 'member' },
    ],
    via: 'jwt',
    scopes: ['read', 'write'],
    ...overrides,
  };
}

interface FakeDatabase {
  db: Database;
  /** Transactions opened. `executeMutation` opens exactly one per write. */
  transactions: number;
}

/**
 * Reads answer from a fixed table of rows; writes are counted and refused.
 *
 * The refusal matters as much as the count: a test that let a write "succeed"
 * against a fake would be asserting on the fake. Anything that gets as far as
 * opening a transaction here fails loudly.
 */
function fakeDatabase(rows: Record<string, Record<string, unknown>[]>): FakeDatabase {
  const state: FakeDatabase = { transactions: 0, db: undefined as unknown as Database };
  const selection = (table: Table) => ({
    where: () => ({
      limit: async () => rows[getTableName(table)] ?? [],
    }),
  });
  // The shape drizzle exposes is far wider than the four calls these tools
  // make, so the cast is to the handle rather than to `any` at each call site.
  state.db = {
    select: () => ({ from: (table: Table) => selection(table) }),
    transaction: async () => {
      state.transactions += 1;
      throw new Error('the fake database refuses to write');
    },
  } as unknown as Database;
  return state;
}

function tool(tools: ReturnType<typeof createPigWriteTools>, name: string) {
  const found = tools.find((candidate) => candidate.name === name);
  assert.ok(found, `${name} is not among ${tools.map((t) => t.name).join(', ')}`);
  return found;
}

function detailsOf(result: { details: unknown }): PigWriteDetails {
  return result.details as PigWriteDetails;
}

function textOf(result: AgentToolResult<unknown>): string {
  const [first] = result.content;
  return first?.type === 'text' ? first.text : '';
}

test('read_only mode offers no write tool at all', () => {
  const { db } = fakeDatabase({});
  const tools = createPigWriteTools({
    db,
    principal: seller(),
    mode: 'read_only',
    propose: async () => 'apply',
  });
  assert.deepEqual(tools, [], 'a read-only session must not be told writes are possible');
});

test('the write surface is exactly five pig_ tools, each teachable to the model', () => {
  const { db } = fakeDatabase({});
  const tools = createPigWriteTools({
    db,
    principal: seller(),
    mode: 'confirm',
    propose: async () => 'apply',
  });

  assert.deepEqual(
    tools.map((candidate) => candidate.name).sort(),
    [
      'pig_create_contact',
      'pig_create_task',
      'pig_log_activity',
      'pig_update_deal_stage',
      'pig_update_record_fields',
    ],
    'the write surface is closed, and grows only by decision',
  );
  for (const candidate of tools) {
    // Without a snippet the tool is absent from the system prompt's tool list.
    assert.ok(candidate.promptSnippet, `${candidate.name} has no promptSnippet`);
    assert.ok(candidate.promptGuidelines?.length, `${candidate.name} teaches the model nothing`);
  }
});

test('a confirm-mode write proposes first and touches nothing until it is answered', async () => {
  const state = fakeDatabase({
    accounts: [{ name: 'Northwind Robotics' }],
  });
  const proposed: Omit<PiggyProposedChange, 'id'>[] = [];
  let released: ((decision: PiggyApprovalDecision) => void) | undefined;

  const tools = createPigWriteTools({
    db: state.db,
    principal: seller(),
    mode: 'confirm',
    propose: async (change) => {
      proposed.push(change);
      // Held open, so the assertions below run at the exact moment a user is
      // still looking at the card: the point at which nothing may have been
      // written yet.
      return new Promise<PiggyApprovalDecision>((resolve) => {
        released = resolve;
      });
    },
  });

  const running = tool(tools, 'pig_log_activity').execute(
    'call-1',
    {
      type: 'call',
      subject: 'Pricing call with procurement',
      body: 'They want H200 pricing before the board meets.',
      accountId: ACCOUNT_ID,
    },
    undefined,
    undefined,
    ctx,
  );

  // Let the proposal be raised, then look at the world before answering.
  await new Promise((resolve) => setImmediate(resolve));
  assert.equal(proposed.length, 1, 'the change was proposed');
  assert.equal(state.transactions, 0, 'no transaction was opened while the user was deciding');

  const [change] = proposed;
  assert.ok(change);
  assert.equal(change.tool, 'pig_log_activity');
  assert.equal(change.kind, 'activity');
  assert.equal(change.summary, 'Log a call on Northwind Robotics');
  assert.equal(change.record?.label, 'Northwind Robotics', 'the card names the record, not a uuid');
  assert.deepEqual(
    change.fields.map((field) => field.label),
    ['Type', 'Subject', 'Note'],
    'the card shows the change field by field',
  );

  assert.ok(released, 'propose was never called');
  released('reject');
  const result = await running;

  assert.equal(state.transactions, 0, 'a rejected change never reaches the database');
  assert.equal(detailsOf(result).status, 'declined');
  assert.match(
    textOf(result),
    /NOT SAVED/,
    'the model is told plainly that nothing was written',
  );
  assert.match(textOf(result), /declined/i);
});

test('a stage change shows the value it is replacing, because a diff needs both', async () => {
  const state = fakeDatabase({
    demand_deals: [{ name: 'Northwind — H200 reserved', stage: 'proposal' }],
  });
  const proposed: Omit<PiggyProposedChange, 'id'>[] = [];
  const tools = createPigWriteTools({
    db: state.db,
    principal: seller(),
    mode: 'confirm',
    propose: async (change) => {
      proposed.push(change);
      return 'reject';
    },
  });

  await tool(tools, 'pig_update_deal_stage').execute(
    'call-2',
    {
      dealType: 'demand',
      dealId: DEAL_ID,
      stage: 'procurement',
      reason: 'Legal cleared the MSA this morning.',
    },
    undefined,
    undefined,
    ctx,
  );

  const [change] = proposed;
  assert.ok(change);
  assert.deepEqual(change.fields[0], {
    label: 'Stage',
    value: 'Procurement',
    previous: 'Proposal',
  });
  assert.equal(state.transactions, 0);
});

test('auto mode writes without asking, because none of these kinds is guarded', async () => {
  const state = fakeDatabase({ accounts: [{ name: 'Northwind Robotics' }] });
  let asked = 0;
  const tools = createPigWriteTools({
    db: state.db,
    principal: seller(),
    mode: 'auto',
    propose: async () => {
      asked += 1;
      return 'apply';
    },
  });

  // The fake refuses every write, which is the point: what is asserted is that
  // the tool got as far as opening a transaction with nobody asked.
  await assert.rejects(
    () =>
      tool(tools, 'pig_log_activity').execute(
        'call-3',
        { type: 'note', subject: 'Left a voicemail', accountId: ACCOUNT_ID },
        undefined,
        undefined,
        ctx,
      ),
    /refuses to write/,
  );
  assert.equal(asked, 0, 'auto mode does not ask for an ordinary activity');
  assert.equal(state.transactions, 1, 'auto mode goes straight to the write');
});

test('a capability failure is reported to the model, not thrown into the stream', async () => {
  const state = fakeDatabase({ accounts: [{ name: 'Northwind Robotics' }] });
  const tools = createPigWriteTools({
    db: state.db,
    // A read-only credential in a session the user put into auto mode. The
    // permission is the user's own, so this is an answer, not a fault.
    principal: seller({ scopes: ['read'] }),
    mode: 'auto',
    propose: async () => 'apply',
  });

  const result = await tool(tools, 'pig_log_activity').execute(
    'call-4',
    { type: 'note', subject: 'Left a voicemail', accountId: ACCOUNT_ID },
    undefined,
    undefined,
    ctx,
  );

  assert.equal(state.transactions, 0, 'permission is checked before any transaction opens');
  assert.equal(detailsOf(result).status, 'refused');
  assert.equal(detailsOf(result).reason, 'insufficient_scope');
  assert.match(textOf(result), /NOT SAVED/);
  assert.match(textOf(result), /permission/i);
});

test('a capability the user lacks on this team is an answer, not a crash', async () => {
  const state = fakeDatabase({
    demand_deals: [{ name: 'Northwind — H200 reserved', stage: 'proposal' }],
  });
  const tools = createPigWriteTools({
    db: state.db,
    // Supply-side only. `updateDemandDealMutationDefinition` requires
    // `deal:write` on `demand`, so this is the everyday case of a person being
    // asked to move somebody else's deal — not a misconfiguration.
    principal: seller({ teams: [{ team: 'supply', role: 'member' }] }),
    mode: 'auto',
    propose: async () => 'apply',
  });

  const result = await tool(tools, 'pig_update_deal_stage').execute(
    'call-8',
    {
      dealType: 'demand',
      dealId: DEAL_ID,
      stage: 'procurement',
      reason: 'They asked me to move it.',
    },
    undefined,
    undefined,
    ctx,
  );

  assert.equal(state.transactions, 0, 'permission is checked before any transaction opens');
  assert.equal(detailsOf(result).status, 'refused');
  assert.equal(detailsOf(result).reason, 'insufficient_permission');
  // Thrown, this would end the turn on the user's own permissions, which reads
  // to them as Piggy being broken rather than as PIG saying no.
  assert.match(textOf(result), /NOT SAVED/);
  assert.match(textOf(result), /deal:write/);
  assert.match(textOf(result), /do not retry it/);
});

test('every kind the write surface proposes is one auto mode may apply', async () => {
  const state = fakeDatabase({
    accounts: [{ name: 'Northwind Robotics' }],
    demand_deals: [{ name: 'Northwind — H200 reserved', stage: 'proposal' }],
  });
  const kinds = new Map<string, string>();
  const tools = createPigWriteTools({
    db: state.db,
    principal: seller(),
    mode: 'confirm',
    propose: async (change) => {
      kinds.set(change.tool, change.kind);
      return 'reject';
    },
  });

  // One call per tool, in confirm mode, so each one has to raise a card and
  // name the kind it belongs to.
  const calls: [string, Record<string, unknown>][] = [
    ['pig_log_activity', { type: 'note', subject: 'Left a voicemail', accountId: ACCOUNT_ID }],
    [
      'pig_create_contact',
      { accountId: ACCOUNT_ID, fullName: 'Marta Reyes', role: 'staff', title: 'VP Infrastructure' },
    ],
    [
      'pig_update_deal_stage',
      { dealType: 'demand', dealId: DEAL_ID, stage: 'procurement', reason: 'Legal cleared it.' },
    ],
    [
      'pig_update_record_fields',
      { recordType: 'account', recordId: ACCOUNT_ID, reason: 'Corrected on the call.', country: 'Germany' },
    ],
    ['pig_create_task', { title: 'Send the H200 quote', startsAt: '2026-09-01', accountId: ACCOUNT_ID }],
  ];
  for (const [name, params] of calls) {
    await tool(tools, name).execute('call-kind', params, undefined, undefined, ctx);
  }

  assert.deepEqual(
    Object.fromEntries([...kinds].sort()),
    {
      pig_create_contact: 'contact',
      pig_create_task: 'task',
      pig_log_activity: 'activity',
      pig_update_deal_stage: 'deal',
      pig_update_record_fields: 'record',
    },
    'every write tool proposes a kind, and the kind is what the policy is read against',
  );
  assert.equal(state.transactions, 0, 'the whole sweep was declined, so nothing was written');

  // `requiresApproval` is the single source of truth for the policy, so the
  // claim "auto mode writes these without asking" is checked against it rather
  // than restated here. A kind added to `PIGGY_ALWAYS_CONFIRM_KINDS` that a
  // tool already uses would flip one of these and fail loudly.
  for (const kind of kinds.values()) {
    assert.equal(isGuardedKind(kind), false, `${kind} is a guarded kind`);
    assert.equal(requiresApproval('auto', kind), false);
    assert.equal(requiresApproval('confirm', kind), true);
    assert.equal(requiresApproval('read_only', kind), true);
  }
});

test('contracts, commitments, allocations and compliance stop even in auto mode', () => {
  // No tool in `write-tools.ts` creates one of these today, and that is the
  // point: the policy is stated once, in the protocol, so a tool added later
  // inherits it rather than having to remember it. This is the assertion that
  // makes `requiresApproval` the single source of truth rather than a comment.
  assert.deepEqual(
    [...PIGGY_ALWAYS_CONFIRM_KINDS],
    ['contract', 'commitment', 'allocation', 'compliance'],
  );
  for (const kind of PIGGY_ALWAYS_CONFIRM_KINDS) {
    assert.equal(isGuardedKind(kind), true);
    assert.equal(requiresApproval('auto', kind), true, `${kind} slipped through auto mode`);
    assert.equal(requiresApproval('confirm', kind), true);
    assert.equal(requiresApproval('read_only', kind), true);
  }
  // And an unguarded kind is only free in auto mode, never in the other two.
  assert.equal(requiresApproval('auto', 'activity'), false);
  assert.equal(requiresApproval('confirm', 'activity'), true);
});

test('an activity with nothing to attach to is refused before it is proposed', async () => {
  const state = fakeDatabase({});
  let asked = 0;
  const tools = createPigWriteTools({
    db: state.db,
    principal: seller(),
    mode: 'confirm',
    propose: async () => {
      asked += 1;
      return 'apply';
    },
  });

  const result = await tool(tools, 'pig_log_activity').execute(
    'call-5',
    { type: 'note', subject: 'Nobody in particular' },
    undefined,
    undefined,
    ctx,
  );

  assert.equal(asked, 0, 'the user is not asked to approve a change that cannot be made');
  assert.equal(state.transactions, 0);
  assert.equal(detailsOf(result).status, 'refused');
  assert.equal(detailsOf(result).reason, 'no_target');
});

test('a field that does not belong to the record type is named, not silently dropped', async () => {
  const state = fakeDatabase({ accounts: [{ name: 'Northwind Robotics' }] });
  const tools = createPigWriteTools({
    db: state.db,
    principal: seller(),
    mode: 'confirm',
    propose: async () => 'apply',
  });

  const result = await tool(tools, 'pig_update_record_fields').execute(
    'call-6',
    {
      recordType: 'account',
      recordId: ACCOUNT_ID,
      reason: 'Correcting after the call.',
      probability: 0.4,
    },
    undefined,
    undefined,
    ctx,
  );

  assert.equal(state.transactions, 0);
  assert.equal(detailsOf(result).reason, 'field_not_applicable');
  assert.match(textOf(result), /probability/);
});

test('an unanswered proposal expires as a rejection rather than holding the turn open', async () => {
  const state = fakeDatabase({ accounts: [{ name: 'Northwind Robotics' }] });
  const tools = createPigWriteTools({
    db: state.db,
    principal: seller(),
    mode: 'confirm',
    // The user closed the tab. Nothing will ever resolve this.
    propose: () => new Promise<PiggyApprovalDecision>(() => {}),
  });

  const abort = new AbortController();
  const running = tool(tools, 'pig_log_activity').execute(
    'call-7',
    { type: 'note', subject: 'Left a voicemail', accountId: ACCOUNT_ID },
    abort.signal,
    undefined,
    ctx,
  );
  // The five-minute deadline is the backstop; an aborted turn must settle at
  // once rather than waiting it out, because the connection is billed either
  // way and nobody is reading the answer.
  abort.abort();

  const result = await running;
  assert.equal(state.transactions, 0);
  assert.equal(detailsOf(result).status, 'declined');
});
