/**
 * Tests for the calendar boundary.
 *
 * The projection itself is exercised against a real Postgres by the seeded
 * demo book; what is pinned here are the decisions that would otherwise fail
 * silently — a mistyped `kinds` filter that looks like a quiet quarter, an
 * authorization gate that mistakes authentication for permission, and the
 * relationship checks that the nullable foreign keys cannot enforce
 * themselves.
 */
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { Database } from '@pig/db';
import { AuthError, type Principal } from '../src/lib/auth';
import { MutationError, executeMutation } from '../src/lib/mutation';
import {
  calendarReadAllowed,
  createEntryMutationDefinition,
  deleteEntryMutationDefinition,
  entriesQuerySchema,
  parseKinds,
  querySchema,
  requireCalendarWrite,
} from '../src/routes/calendar';

function principal(overrides: Partial<Principal> = {}): Principal {
  return {
    userId: '10000000-0000-4000-8000-000000000001',
    email: 'seller@example.com',
    name: 'Seller',
    isPlatformAdmin: false,
    teams: [{ team: 'demand', role: 'member' }],
    via: 'jwt',
    scopes: ['read', 'write'],
    ...overrides,
  };
}

describe('calendar read boundary', () => {
  it('requires an explicit read scope rather than treating a token as permission', () => {
    assert.equal(calendarReadAllowed([]), false);
    assert.equal(calendarReadAllowed(['write']), false);
    assert.equal(calendarReadAllowed(['read']), true);
  });
});

describe('calendar write boundary', () => {
  it('accepts either pipeline, because a dated item belongs to whoever runs the motion', () => {
    assert.doesNotThrow(() =>
      requireCalendarWrite(principal({ teams: [{ team: 'demand', role: 'member' }] })),
    );
    assert.doesNotThrow(() =>
      requireCalendarWrite(principal({ teams: [{ team: 'supply', role: 'member' }] })),
    );
  });

  it('refuses a read-only credential even when its owner has the role', () => {
    // The credential's scope caps the person's authority; an agent key issued
    // for reading must not be able to write because a human somewhere may.
    assert.throws(
      () => requireCalendarWrite(principal({ scopes: ['read'] })),
      (error: unknown) =>
        error instanceof AuthError && error.code === 'insufficient_scope',
    );
  });

  it('refuses a member of neither pipeline', () => {
    assert.throws(
      () => requireCalendarWrite(principal({ teams: [{ team: 'research', role: 'admin' }] })),
      (error: unknown) =>
        error instanceof AuthError && error.code === 'insufficient_permission',
    );
  });
});

describe('kinds filter', () => {
  it('rejects an unknown kind rather than returning nothing', () => {
    // A typo that silently filters everything out is indistinguishable from a
    // genuinely empty quarter, which is the worst possible failure for a view
    // whose whole job is to show what is coming.
    assert.throws(
      () => parseKinds('renewal'),
      (error: unknown) => error instanceof MutationError && error.code === 'invalid_kinds',
    );
    assert.throws(() => parseKinds('obligation_due,expected_clos'), MutationError);
  });

  it('treats absent and empty as no filter at all', () => {
    assert.equal(parseKinds(undefined), undefined);
    assert.equal(parseKinds(''), undefined);
    assert.equal(parseKinds(' , '), undefined);
  });

  it('accepts a spaced list of known kinds', () => {
    assert.deepEqual(parseKinds('obligation_due, renewal_notice'), [
      'obligation_due',
      'renewal_notice',
    ]);
  });
});

describe('calendar query validation', () => {
  it('rejects a malformed account id on both reads, not just one', () => {
    // Fed straight into `eq()` on a uuid column, `not-a-uuid` came back as a
    // 500 from Postgres 22P02. The two endpoints take the identical parameter
    // and must answer it identically.
    assert.equal(querySchema.safeParse({ accountId: 'not-a-uuid' }).success, false);
    assert.equal(entriesQuerySchema.safeParse({ accountId: 'not-a-uuid' }).success, false);
    assert.equal(
      entriesQuerySchema.safeParse({ accountId: '30000000-0000-4000-8000-000000000003' })
        .success,
      true,
    );
    assert.equal(entriesQuerySchema.safeParse({}).success, true);
  });

  it('rejects a time zone the runtime cannot use rather than silently answering in UTC', () => {
    // The cache in @pig/core is keyed on this string, so an unvalidated one is
    // both a wrong answer and a way to make a long-lived process grow.
    assert.equal(querySchema.safeParse({ timezone: 'Mars/Olympus' }).success, false);
    assert.equal(querySchema.safeParse({ timezone: 'Europe/London' }).success, true);
  });
});

/** A transaction stub that records the order of writes, as in capacity-writes. */
function recordingDb(rows: {
  select?: unknown[];
  insertReturns?: unknown[];
  deleteReturns?: unknown[];
}) {
  const events: string[] = [];
  const tx = {
    select: () => ({
      from: () => ({
        where: () => ({
          limit: async () => {
            events.push('select');
            return rows.select ?? [];
          },
        }),
      }),
    }),
    insert: () => ({
      // A thenable rather than a promise: the audit write is awaited directly
      // while the entity write goes through `.returning()`, and constructing a
      // real promise here would record the audit write that never happened.
      values: (values: Record<string, unknown>) => {
        const record = () => events.push('subject' in values ? 'activity' : 'insert');
        return {
          then: (resolve: (value: unknown) => unknown) => {
            record();
            return Promise.resolve().then(() => resolve(undefined));
          },
          returning: async () => {
            record();
            return rows.insertReturns ?? [];
          },
        };
      },
    }),
    delete: () => ({
      where: () => ({
        returning: async () => {
          events.push('delete');
          return rows.deleteReturns ?? [];
        },
      }),
    }),
  };
  const db = {
    transaction: async (work: (transaction: unknown) => Promise<unknown>) => {
      events.push('begin');
      const result = await work(tx);
      events.push('commit');
      return result;
    },
  } as unknown as Database;
  return { db, events };
}

describe('calendar entry mutation', () => {
  const entry = {
    id: '20000000-0000-4000-8000-000000000002',
    title: 'Q business review',
    kind: 'qbr' as const,
    accountId: null,
    demandDealId: null,
    supplyDealId: null,
    startsAt: new Date('2026-09-03T14:00:00.000Z'),
    endsAt: new Date('2026-09-03T15:30:00.000Z'),
    completedAt: null,
  };

  it('writes the entry and its audit event inside one transaction', async () => {
    const { db, events } = recordingDb({ insertReturns: [entry] });
    const created = await executeMutation(
      db,
      principal(),
      async () => ({
        title: 'Q business review',
        kind: 'qbr',
        startsAt: '2026-09-03T14:00:00.000Z',
        endsAt: '2026-09-03T15:30:00.000Z',
      }),
      createEntryMutationDefinition(),
    );
    assert.equal(created.id, entry.id);
    assert.deepEqual(events, ['begin', 'insert', 'activity', 'commit']);
  });

  it('defaults the owner to the author, because unassigned work is work nobody does', async () => {
    let written: Record<string, unknown> | undefined;
    const capturing = {
      transaction: async (work: (transaction: unknown) => Promise<unknown>) =>
        work({
          insert: () => ({
            values: (values: Record<string, unknown>) => {
              written ??= values;
              return Object.assign(Promise.resolve(), {
                returning: async () => [entry],
              });
            },
          }),
        }),
    } as unknown as Database;
    await executeMutation(
      capturing,
      principal(),
      async () => ({ title: 'Reminder', startsAt: '2026-09-03T14:00:00.000Z' }),
      createEntryMutationDefinition(),
    );
    assert.equal(written?.ownerUserId, '10000000-0000-4000-8000-000000000001');
    assert.equal(written?.createdByUserId, '10000000-0000-4000-8000-000000000001');
  });

  it('refuses a window that ends before it starts', async () => {
    const { db } = recordingDb({ insertReturns: [entry] });
    await assert.rejects(
      executeMutation(
        db,
        principal(),
        async () => ({
          title: 'Backwards',
          startsAt: '2026-09-03T16:00:00.000Z',
          endsAt: '2026-09-03T14:00:00.000Z',
        }),
        createEntryMutationDefinition(),
      ),
      (error: unknown) => error instanceof MutationError && error.code === 'invalid_window',
    );
  });

  it('refuses a deal that belongs to a different account', async () => {
    // Nothing in the schema can catch this: both columns are independently
    // nullable foreign keys, so the disagreement is only visible here.
    const { db } = recordingDb({
      select: [{ accountId: '90000000-0000-4000-8000-000000000009' }],
    });
    await assert.rejects(
      executeMutation(
        db,
        principal(),
        async () => ({
          title: 'Mismatch',
          startsAt: '2026-09-03T14:00:00.000Z',
          accountId: '30000000-0000-4000-8000-000000000003',
          demandDealId: '40000000-0000-4000-8000-000000000004',
        }),
        createEntryMutationDefinition(),
      ),
      (error: unknown) =>
        error instanceof MutationError && error.code === 'relationship_mismatch',
    );
  });

  it('reports a stale owner as 404, the way every other reference here does', async () => {
    // The column is a foreign key with no check in front of it, so assigning
    // to a user who has been removed produced a 500 from the constraint. It is
    // an ordinary client mistake and deserves the ordinary answer.
    const { db } = recordingDb({ select: [], insertReturns: [entry] });
    await assert.rejects(
      executeMutation(
        db,
        principal(),
        async () => ({
          title: 'Handover',
          startsAt: '2026-09-03T14:00:00.000Z',
          ownerUserId: '50000000-0000-4000-8000-000000000005',
        }),
        createEntryMutationDefinition(),
      ),
      (error: unknown) => error instanceof MutationError && error.status === 404,
    );
  });

  it('does not re-read the author when it defaults the owner to them', async () => {
    // The request already proved that user exists; a lookup per create to
    // confirm it would be a query bought with nothing.
    const { db, events } = recordingDb({ insertReturns: [entry] });
    await executeMutation(
      db,
      principal(),
      async () => ({ title: 'Reminder', startsAt: '2026-09-03T14:00:00.000Z' }),
      createEntryMutationDefinition(),
    );
    assert.deepEqual(events, ['begin', 'insert', 'activity', 'commit']);
  });

  it('reports a missing entry as 404 rather than a silent no-op delete', async () => {
    const { db } = recordingDb({ deleteReturns: [] });
    await assert.rejects(
      executeMutation(
        db,
        principal(),
        async () => ({}),
        deleteEntryMutationDefinition(),
        { id: '20000000-0000-4000-8000-000000000002' },
      ),
      (error: unknown) =>
        error instanceof MutationError && error.status === 404,
    );
  });
});
