/**
 * The page tools, executed against a real Postgres.
 *
 * `test/chat-tools.test.ts` passes `{} as Database` and asserts on tool names,
 * which is the right shape for a selection test and no shape at all for the
 * five `execute` bodies underneath: every where clause, every `Number(gpuHours)`
 * coercion and every date comparison was covered by tsc alone. The defect that
 * prompted this suite — a headline quoting `list.length` from a capped query as
 * if it were a total — typechecks perfectly.
 *
 * It lives in `e2e/` rather than `test/` for one reason: the unit suite runs in
 * CI BEFORE the migration step, against a database with no tables. `test:e2e`
 * runs after migrate and seed, which is the only point at which a query here
 * can mean anything.
 *
 * Every assertion is a DELTA against a reading taken before the fixture is
 * inserted. The tools are book-wide by design — there is no tenant to scope
 * them to — so they see the seed, the demo book and whatever the API's own E2E
 * left behind. Absolute figures would be a test of the seed, not of the tool.
 */
import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import test, { after, before } from 'node:test';
import { eq, inArray } from 'drizzle-orm';
import {
  accounts,
  allocations,
  capacityCommitments,
  contractObligations,
  contracts,
  createDatabase,
  demandDeals,
  type Database,
} from '@pig/db';
import { createPagePigTools } from '../src/page-tools';

const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) throw new Error('DATABASE_URL is required for the Piggy page-tool E2E tests.');

const db: Database = createDatabase({ url: databaseUrl, max: 4 });

const MINUTE = 60_000;
const HOUR = 3_600_000;
const DAY = 86_400_000;

/** Marks every fixture row so cleanup can never reach somebody else's data. */
const marker = `PIGGY-E2E-${randomUUID()}`;

/** How many exemplars each result may carry — mirrors EXEMPLARS in page-tools. */
const EXEMPLARS = 8;

interface Reading {
  headline: string;
  truncated?: unknown;
  [key: string]: unknown;
}

async function run(route: '/margin' | '/capacity' | '/demand' | '/calendar' | '/'): Promise<Reading> {
  const [tool] = createPagePigTools(db, route);
  assert.ok(tool, `no tool for ${route}`);
  // The calendar tool is the only one taking input, and its default is 30.
  return (await tool.execute({})) as Reading;
}

const created = {
  accountId: '',
  contractId: '',
  commitmentId: '',
  dealIds: [] as string[],
};

/**
 * The fixture is deliberately lopsided.
 *
 * Twenty obligations fall due inside the horizon and twelve are already late,
 * because the capped exemplar lists hold sixteen and eight — a headline that
 * reports the list length rather than the count cannot survive those numbers.
 */
const UPCOMING_OBLIGATIONS = 20;
const OVERDUE_OBLIGATIONS = 12;

let before30: Reading;
let after30: Reading;
let marginBefore: Reading;
let marginAfter: Reading;
let idleBefore: Reading;
let idleAfter: Reading;
let pipelineBefore: Reading;
let pipelineAfter: Reading;
let workspaceBefore: Reading;
let workspaceAfter: Reading;

before(async () => {
  const now = Date.now();

  [before30, marginBefore, idleBefore, pipelineBefore, workspaceBefore] = await Promise.all([
    run('/calendar'),
    run('/margin'),
    run('/capacity'),
    run('/demand'),
    run('/'),
  ]);

  const [account] = await db
    .insert(accounts)
    .values({ name: `${marker} counterparty`, side: 'supply' })
    .returning();
  assert.ok(account);
  created.accountId = account.id;

  const [contract] = await db
    .insert(contracts)
    .values({
      accountId: account.id,
      type: 'msa',
      status: 'executed',
      side: 'demand',
      title: `${marker} master agreement`,
      // Inside the 30-day horizon, so the projection must emit a
      // contract_expiry — a kind the old two-table version could not see.
      expiresAt: new Date(now + 10 * DAY),
      // Auto-renewal with notice puts a renewal_notice inside the horizon too.
      isAutoRenew: true,
      noticeDays: 5,
    })
    .returning();
  assert.ok(contract);
  created.contractId = contract.id;

  await db.insert(contractObligations).values([
    ...Array.from({ length: UPCOMING_OBLIGATIONS }, (_, i) => ({
      contractId: contract.id,
      title: `${marker} upcoming ${i}`,
      kind: 'milestone',
      dueAt: new Date(now + (i + 1) * MINUTE),
    })),
    ...Array.from({ length: OVERDUE_OBLIGATIONS }, (_, i) => ({
      contractId: contract.id,
      title: `${marker} overdue ${i}`,
      kind: 'milestone',
      // i = 0 lapsed a minute ago, i = 11 twelve minutes ago.
      dueAt: new Date(now - (i + 1) * MINUTE),
    })),
    {
      contractId: contract.id,
      title: `${marker} already done`,
      kind: 'milestone',
      dueAt: new Date(now + 2 * DAY),
      // Completed work is a dated fact, not a workload; it must not be counted.
      completedAt: new Date(now - DAY),
    },
  ]);

  // 1,000 GPU-hours bought at 100c. Half sells at exactly cost, 100 more are
  // held; the block therefore loses money on the hours nobody bought, which is
  // the arithmetic PIG exists to keep honest.
  const [commitment] = await db
    .insert(capacityCommitments)
    .values({
      accountId: account.id,
      name: `${marker} block`,
      gpuType: 'H100',
      gpuCount: 8,
      startsAt: new Date(now - DAY),
      endsAt: new Date(now + 20 * DAY),
      totalGpuHours: '1000.00',
      costPerGpuHourCents: 100,
    })
    .returning();
  assert.ok(commitment);
  created.commitmentId = commitment.id;

  const [openDeal, closingDeal] = await db
    .insert(demandDeals)
    .values([
      {
        accountId: account.id,
        name: `${marker} open deal`,
        stage: 'proposal',
        acvCents: 1_000_000,
        tcvCents: 2_500_000,
      },
      {
        accountId: account.id,
        name: `${marker} closing deal`,
        stage: 'procurement',
        acvCents: 4_000_000,
        tcvCents: 4_000_000,
        probability: '0.50',
        expectedCloseDate: new Date(now + 3 * DAY),
      },
    ])
    .returning();
  assert.ok(openDeal && closingDeal);
  created.dealIds = [openDeal.id, closingDeal.id];

  await db.insert(allocations).values([
    {
      capacityCommitmentId: commitment.id,
      demandDealId: openDeal.id,
      gpuHours: '500.00',
      pricePerGpuHourCents: 100,
      startsAt: new Date(now - HOUR),
      endsAt: new Date(now + 10 * DAY),
      status: 'committed',
    },
    {
      capacityCommitmentId: commitment.id,
      demandDealId: closingDeal.id,
      gpuHours: '100.00',
      pricePerGpuHourCents: 300,
      startsAt: new Date(now - HOUR),
      endsAt: new Date(now + 10 * DAY),
      status: 'planned',
      // A live hold: removed from availability, never revenue. Inside the
      // horizon, so it is also a hold_expiry event on the calendar.
      holdExpiresAt: new Date(now + 4 * DAY),
    },
  ]);

  [after30, marginAfter, idleAfter, pipelineAfter, workspaceAfter] = await Promise.all([
    run('/calendar'),
    run('/margin'),
    run('/capacity'),
    run('/demand'),
    run('/'),
  ]);
});

after(async () => {
  if (created.commitmentId) {
    await db
      .delete(allocations)
      .where(eq(allocations.capacityCommitmentId, created.commitmentId));
    await db.delete(capacityCommitments).where(eq(capacityCommitments.id, created.commitmentId));
  }
  if (created.dealIds.length > 0) {
    await db.delete(demandDeals).where(inArray(demandDeals.id, created.dealIds));
  }
  // Obligations cascade from the contract.
  if (created.contractId) await db.delete(contracts).where(eq(contracts.id, created.contractId));
  if (created.accountId) await db.delete(accounts).where(eq(accounts.id, created.accountId));
  await db.$client.end();
});

// ---------------------------------------------------------------------------
// The calendar
// ---------------------------------------------------------------------------

interface CalendarReading extends Reading {
  exactTotals: { obligationsDue: number; dealsExpectedToClose: number };
  upcoming: { count: number; byKind: Record<string, number>; events: { startsAt: string }[] };
  overdue: {
    count: number;
    byKind: Record<string, number>;
    events: { title: string; startsAt: string }[];
  };
}

test('the calendar headline counts the whole set, not the capped exemplar list', () => {
  const from = before30 as CalendarReading;
  const to = after30 as CalendarReading;

  // The defect, pinned. Twenty obligations were added and the exemplar list
  // holds sixteen; a headline built from `list.length` reports sixteen.
  assert.equal(
    to.exactTotals.obligationsDue - from.exactTotals.obligationsDue,
    UPCOMING_OBLIGATIONS,
  );
  assert.equal(
    (to.upcoming.byKind.obligation_due ?? 0) - (from.upcoming.byKind.obligation_due ?? 0),
    UPCOMING_OBLIGATIONS,
  );
  assert.equal(to.upcoming.events.length, EXEMPLARS * 2);
  assert.ok(to.upcoming.count > to.upcoming.events.length);
  assert.match(to.headline, new RegExp(`${to.exactTotals.obligationsDue} obligation\\(s\\) due`));

  assert.equal(
    (to.overdue.byKind.obligation_due ?? 0) - (from.overdue.byKind.obligation_due ?? 0),
    OVERDUE_OBLIGATIONS,
  );
  assert.equal(to.overdue.events.length, EXEMPLARS);
  assert.ok(to.overdue.count > to.overdue.events.length);
  assert.match(to.headline, new RegExp(`${to.overdue.count} item\\(s\\) overdue`));
});

test('a completed obligation is a dated fact, not a workload', () => {
  const to = after30 as CalendarReading;
  const from = before30 as CalendarReading;
  // Twenty-one obligations were inserted inside the horizon; the completed one
  // is excluded from both the SQL total and the state-filtered list.
  assert.equal(
    to.exactTotals.obligationsDue - from.exactTotals.obligationsDue,
    UPCOMING_OBLIGATIONS,
  );
  const states = (to.upcoming as unknown as { byState: Record<string, number> }).byState;
  assert.equal(states.done, undefined);
});

test('the projection reaches the kinds the old two-table version could not', () => {
  const to = after30 as CalendarReading;
  const from = before30 as CalendarReading;
  const gained = (kind: string): number =>
    (to.upcoming.byKind[kind] ?? 0) - (from.upcoming.byKind[kind] ?? 0);

  assert.equal(gained('contract_expiry'), 1);
  assert.equal(gained('renewal_notice'), 1);
  assert.equal(gained('hold_expiry'), 1);
  assert.equal(gained('expected_close'), 1);
  // The commitment window overlaps the horizon on both edges.
  assert.ok(gained('capacity_window') >= 1);
  assert.ok(gained('allocation_window') >= 1);
  assert.equal(to.exactTotals.dealsExpectedToClose - from.exactTotals.dealsExpectedToClose, 1);
});

test('overdue exemplars are the most recently lapsed, not the oldest in the book', () => {
  const to = after30 as CalendarReading;
  const dates = to.overdue.events.map((event) => event.startsAt);
  assert.deepEqual(dates, [...dates].sort().reverse(), 'overdue exemplars run newest first');

  const titles = to.overdue.events.map((event) => event.title);
  // Lapsed one minute ago: present. Lapsed twelve minutes ago: cut, because
  // twelve rows were inserted and only eight are carried.
  assert.ok(titles.includes(`${marker} overdue 0`));
  assert.ok(!titles.includes(`${marker} overdue ${OVERDUE_OBLIGATIONS - 1}`));
});

test('a book this size is not truncated, and says so', () => {
  assert.equal(after30.truncated, false);
  assert.doesNotMatch(after30.headline, /at least/);
});

// ---------------------------------------------------------------------------
// The book
// ---------------------------------------------------------------------------

interface MarginReading extends Reading {
  totals: { revenueCents: number; costCents: number; grossMarginCents: number };
  liveCommitments: number;
}

test('margin charges cost against the full commitment and coerces numeric strings', () => {
  const from = marginBefore as MarginReading;
  const to = marginAfter as MarginReading;

  // 500 sold hours × 100c. The held 100 are not revenue.
  assert.equal(to.totals.revenueCents - from.totals.revenueCents, 50_000);
  // 1,000 committed hours × 100c — not the 500 that sold.
  assert.equal(to.totals.costCents - from.totals.costCents, 100_000);
  // Charging cost against the sold hours alone would report break-even here
  // instead of a 50,000c hole, which is the reading the rule forbids.
  assert.equal(to.totals.grossMarginCents - from.totals.grossMarginCents, -50_000);
  assert.equal(to.liveCommitments - from.liveCommitments, 1);

  // `gpuHours` arrives as a string. Concatenation would give "1000.00500.00"
  // and a revenue in the billions rather than a delta of exactly 50,000c.
  assert.ok(Number.isSafeInteger(to.totals.revenueCents));
});

test('the idle block appears with its break-even price', () => {
  const from = idleBefore as Reading & { totalIdleCostCents: number; blocks: unknown[] };
  const to = idleAfter as Reading & {
    totalIdleCostCents: number;
    blocks: { name: string; idleGpuHours: number; breakEvenPricePerGpuHourCents: number }[];
  };

  // 50% unsold, well past the 25% threshold: 500 idle hours at 100c.
  assert.equal(to.totalIdleCostCents - from.totalIdleCostCents, 50_000);
  const mine = to.blocks.find((block) => block.name === `${marker} block`);
  assert.ok(mine, 'the fixture block is idle enough to be listed');
  assert.equal(mine.idleGpuHours, 500);
  // The remaining 500 hours must fetch 100c each to cover the whole block.
  assert.equal(mine.breakEvenPricePerGpuHourCents, 100);
});

// ---------------------------------------------------------------------------
// The pipelines
// ---------------------------------------------------------------------------

interface PipelineReading extends Reading {
  demand: { openDeals: number; valueCents: number; byStage: Record<string, number> };
  supply: { openDeals: number };
}

test('the pipeline totals TCV where it is known and reports open stages', () => {
  const from = pipelineBefore as PipelineReading;
  const to = pipelineAfter as PipelineReading;

  assert.equal(to.demand.openDeals - from.demand.openDeals, 2);
  // 2,500,000 + 4,000,000, both by TCV.
  assert.equal(to.demand.valueCents - from.demand.valueCents, 6_500_000);
  assert.equal((to.demand.byStage.proposal ?? 0) - (from.demand.byStage.proposal ?? 0), 1);
  assert.equal((to.demand.byStage.procurement ?? 0) - (from.demand.byStage.procurement ?? 0), 1);
  assert.deepEqual(to.truncated, { demandDeals: false, supplyDeals: false });
});

test('the workspace summary agrees with the tools it summarises', () => {
  const from = workspaceBefore as Reading & { book: { costCents: number }; openDemandDeals: number };
  const to = workspaceAfter as Reading & { book: { costCents: number }; openDemandDeals: number };

  assert.equal(to.book.costCents - from.book.costCents, 100_000);
  assert.equal(to.openDemandDeals - from.openDemandDeals, 2);
  assert.deepEqual(to.truncated, {
    commitments: false,
    demandDeals: false,
    supplyDeals: false,
  });
});
