/**
 * The first tests that go through `createApp()`.
 *
 * Every other test in this directory calls a mutation definition, or a helper,
 * directly. That checks the rule and skips the wiring — and the wiring is where
 * this codebase has actually been wrong: a guard mounted after its handler
 * never runs, an AuthError thrown inside a mounted sub-app has to reach the
 * parent's `onError` to become a 403 rather than a 500, and a route added to
 * the public allowlist by mistake is invisible to a unit test. `grep createApp
 * apps/api/test` used to return nothing.
 *
 * So these assert on status codes and error envelopes over real HTTP, and
 * nothing else. They are deliberately cheap: no Postgres, a fake that answers
 * only the handful of queries authentication and the read guard reach.
 */
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { Team, TeamRole } from '@pig/core';
import type { Database } from '@pig/db';
import { apiKeys, teamMemberships, users } from '@pig/db';
import { createApp } from '../src/app';
import type { AuthProvider } from '../src/lib/auth-provider';
import { hashApiKey } from '../src/lib/auth';
import { loadConfig, type Config } from '../src/lib/config';

const USER_ID = '00000000-0000-4000-8000-0000000000aa';
const SUBJECT = 'auth-subject-1';
const API_KEY = 'pig_test_key_value';

interface Fixture {
  /** Absent means a verified token with no PIG profile — the `needs_profile` case. */
  user?: { id: string; email: string; name: string; authSubject: string; deactivatedAt: Date | null; isPlatformAdmin: boolean };
  memberships?: { team: Team; role: TeamRole }[];
  apiKey?: { scopes: string[] };
}

/**
 * Answers by table identity rather than by call order, because the order in
 * which `loadPrincipal` and a handler query is an implementation detail and a
 * fake that depends on it fails for the wrong reason later.
 */
function fixtureDatabase(fixture: Fixture): Database {
  const userRows = fixture.user ? [fixture.user] : [];
  const membershipRows = fixture.memberships ?? [];
  const keyRows = fixture.apiKey
    ? [{
        id: 'key-1',
        userId: USER_ID,
        keyHash: hashApiKey(API_KEY),
        scopes: fixture.apiKey.scopes,
        revokedAt: null,
        expiresAt: null,
      }]
    : [];

  function rowsFor(table: unknown): unknown[] {
    if (table === users) return userRows;
    if (table === teamMemberships) return membershipRows;
    if (table === apiKeys) return keyRows;
    return [];
  }

  function chain(rows: unknown[]) {
    const self: Record<string, unknown> = {
      leftJoin: () => self,
      innerJoin: () => self,
      where: () => self,
      orderBy: () => self,
      limit: async () => rows,
      then: (resolve: (value: unknown[]) => unknown) => resolve(rows),
    };
    return self;
  }

  return {
    select: () => ({
      from: (table: unknown) => {
        // `/api/team` joins users to memberships and expects the flattened
        // shape, which the users fixture already carries enough of.
        if (table === users) {
          return chain(userRows.map((row) => ({ ...row, team: membershipRows[0]?.team ?? null, role: membershipRows[0]?.role ?? null })));
        }
        return chain(rowsFor(table));
      },
    }),
    update: () => ({ set: () => ({ where: async () => undefined }) }),
    transaction: async (work: (tx: unknown) => Promise<unknown>) => work({}),
  } as unknown as Database;
}

const provider: AuthProvider = {
  name: 'test',
  async verifyAccessToken(token: string) {
    if (token !== 'good-token') throw new Error('bad token');
    return { subject: SUBJECT, email: 'seller@example.com' };
  },
};

function config(): Config {
  // A real `loadConfig`, not a literal: the production guards live in it, and a
  // hand-rolled Config object would let this suite pass under a configuration
  // the server would refuse to start on.
  return loadConfig({
    NODE_ENV: 'test',
    DATABASE_URL: 'postgres://pig:pig@localhost:5432/pig-not-connected',
    PIG_PUBLIC_URL: 'http://localhost:8920',
    PIG_ADMIN_EMAILS: '',
  } as NodeJS.ProcessEnv);
}

function member(team: Team, role: TeamRole): Fixture {
  return {
    user: {
      id: USER_ID,
      email: 'seller@example.com',
      name: 'Seller',
      authSubject: SUBJECT,
      deactivatedAt: null,
      isPlatformAdmin: false,
    },
    memberships: [{ team, role }],
  };
}

function request(fixture: Fixture, path: string, init: RequestInit = {}) {
  return createApp(config(), fixtureDatabase(fixture), provider).request(path, init);
}

const bearer = (token: string) => ({ headers: { authorization: `Bearer ${token}` } });

async function envelope(response: Response) {
  return (await response.json()) as { code?: string; error?: string };
}

describe('authentication over HTTP', () => {
  it('answers 401 no_token when nothing is presented', async () => {
    const response = await request(member('demand', 'member'), '/api/dashboard');

    assert.equal(response.status, 401);
    assert.equal((await envelope(response)).code, 'no_token');
  });

  it('answers 401 invalid_token without saying which knob to turn', async () => {
    const response = await request(member('demand', 'member'), '/api/dashboard', bearer('rubbish'));

    assert.equal(response.status, 401);
    assert.equal((await envelope(response)).code, 'invalid_token');
  });

  /**
   * The distinction the whole auth file exists for: the identity provider is
   * shared with another application, so a verified token proves an account
   * somewhere, not membership here.
   */
  it('answers 403 needs_profile for a verified token with no PIG user', async () => {
    const response = await request({}, '/api/team', bearer('good-token'));

    assert.equal(response.status, 403);
    assert.equal((await envelope(response)).code, 'needs_profile');
  });

  it('answers 403 deactivated rather than pretending the account is unknown', async () => {
    const fixture = member('demand', 'member');
    fixture.user!.deactivatedAt = new Date('2026-01-01T00:00:00Z');

    const response = await request(fixture, '/api/team', bearer('good-token'));

    assert.equal(response.status, 403);
    assert.equal((await envelope(response)).code, 'deactivated');
  });

  it('leaves health and config reachable without a token', async () => {
    for (const path of ['/api/health', '/api/config']) {
      const response = await request({}, path);
      assert.equal(response.status, 200, path);
    }
  });
});

describe('credential scope over HTTP', () => {
  it('refuses a write from a read-only API key', async () => {
    const fixture = { ...member('demand', 'admin'), apiKey: { scopes: ['read'] } };

    const response = await request(fixture, '/api/contracts', {
      method: 'POST',
      headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json' },
      body: JSON.stringify({ accountId: USER_ID, type: 'msa', side: 'demand', title: 'MSA' }),
    });

    // Scope, not permission: this person IS a demand admin. The credential
    // they are acting through is what lacks the authority, and saying so is
    // the difference between "ask your administrator" and "use another key".
    assert.equal(response.status, 403);
    assert.equal((await envelope(response)).code, 'insufficient_scope');
  });

  it('admits a read from the same read-only key', async () => {
    const fixture = { ...member('demand', 'admin'), apiKey: { scopes: ['read'] } };

    const response = await request(fixture, '/api/me', {
      headers: { authorization: `Bearer ${API_KEY}` },
    });

    assert.equal(response.status, 200);
  });
});

describe('capability over HTTP', () => {
  it('refuses a write from a viewer', async () => {
    const response = await request(member('demand', 'viewer'), '/api/contracts', {
      method: 'POST',
      headers: { authorization: 'Bearer good-token', 'content-type': 'application/json' },
      body: JSON.stringify({ accountId: USER_ID, type: 'msa', side: 'demand', title: 'MSA' }),
    });

    assert.equal(response.status, 403);
    assert.equal((await envelope(response)).code, 'insufficient_permission');
  });

  it('reports a viewer\'s grants on /api/me as reads only', async () => {
    const response = await request(member('demand', 'viewer'), '/api/me', bearer('good-token'));
    const body = (await response.json()) as { permissions: { capability: string }[] };

    assert.equal(response.status, 200);
    assert.deepEqual(
      body.permissions.map((grant) => grant.capability),
      ['book:read', 'team:read'],
    );
  });

});
