/**
 * API smoke test — exercises every role's endpoints + key workflows against a
 * freshly seeded DB. Run AFTER `npm run seed` and with the API running.
 *
 *   npm run seed && npm run smoke
 *
 * This will mutate data (check-ins, walk-ins). Re-seed before manual UI testing.
 */

/* eslint-disable no-console */

const BASE = process.env.SMOKE_BASE_URL || 'http://localhost:4200/api';

let passed = 0;
let failed = 0;
const failures: string[] = [];

const c = {
  red: (s: string) => `\x1b[31m${s}\x1b[0m`,
  green: (s: string) => `\x1b[32m${s}\x1b[0m`,
  cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
  yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
  dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
};

function section(name: string): void {
  console.log(`\n${c.cyan(`── ${name} ──`)}`);
}

function check(label: string, cond: boolean, detail?: string): boolean {
  if (cond) {
    console.log(`  ${c.green('✔')} ${label}${detail ? c.dim(` — ${detail}`) : ''}`);
    passed++;
    return true;
  } else {
    console.log(`  ${c.red('✘')} ${label}${detail ? c.dim(` — ${detail}`) : ''}`);
    failures.push(label);
    failed++;
    return false;
  }
}

interface ReqOpts {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  body?: unknown;
  token?: string;
  expect?: number;
}

async function req(path: string, opts: ReqOpts = {}): Promise<{ status: number; body: any }> {
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
  if (opts.token) headers.Authorization = `Bearer ${opts.token}`;
  const res = await fetch(`${BASE}${path}`, {
    method: opts.method || 'GET',
    headers,
    body: opts.body ? JSON.stringify(opts.body) : undefined,
  });
  let body: any = null;
  try { body = await res.json(); } catch { /* may be empty */ }
  return { status: res.status, body };
}

async function loginAs(email: string): Promise<{ token: string; role: string } | null> {
  const send = await req('/auth/send-otp', { method: 'POST', body: { email } });
  if (send.status !== 200 || !send.body?.devOtp) return null;
  const verify = await req('/auth/verify-otp', {
    method: 'POST',
    body: { email, otp: send.body.devOtp },
  });
  if (verify.status !== 200 || !verify.body?.token) return null;
  return { token: verify.body.token, role: verify.body.role };
}

async function scannerLogin(mobile: string, password: string): Promise<{ token: string } | null> {
  const r = await req('/visitor-scanner/login', { method: 'POST', body: { mobile, password } });
  if (r.status !== 200 || !r.body?.token) return null;
  return { token: r.body.token };
}

async function main(): Promise<void> {
  console.log(c.cyan(`Smoke pass → ${BASE}`));

  // ───────── Auth ─────────────────────────────────────────────────────────
  section('Auth');

  const sendUnknown = await req('/auth/send-otp', { method: 'POST', body: { email: 'noone@example.com' } });
  check('Unknown email blocked at /send-otp', sendUnknown.status === 403, `status=${sendUnknown.status}`);

  const sendOwner = await req('/auth/send-otp', { method: 'POST', body: { email: 'owner@example.com' } });
  check('Owner /send-otp returns devOtp', sendOwner.status === 200 && !!sendOwner.body?.devOtp);

  const badOtp = await req('/auth/verify-otp', { method: 'POST', body: { email: 'owner@example.com', otp: '000000' } });
  check('Wrong OTP rejected', badOtp.status === 400, `status=${badOtp.status}`);

  const owner = await loginAs('owner@example.com');
  if (!check('Owner login succeeds', !!owner)) { console.log('aborting — cannot continue without owner token'); process.exit(1); }
  check('Owner role correct', owner!.role === 'OWNER', `role=${owner!.role}`);

  const alice = await loginAs('alice@example.com');
  if (!check('Alice (approver) login succeeds', !!alice)) process.exit(1);
  check('Alice role = APPROVER', alice!.role === 'APPROVER', `role=${alice!.role}`);

  const bob = await loginAs('bob@example.com');
  if (!check('Bob (approver) login succeeds', !!bob)) process.exit(1);

  // Carol is inactive — verifyOtp's deactivated-account guard now blocks
  // her at login (403) instead of silently falling through to the
  // auto-Owner-creation path (which used to be a privilege-escalation bug).
  const carol = await loginAs('carol@example.com');
  check('Carol login blocked (inactive approver)', !carol);

  // ───────── Owner endpoints ──────────────────────────────────────────────
  section('Owner endpoints');

  const meO = await req('/auth/me', { token: owner!.token });
  check('GET /auth/me returns owner', meO.status === 200 && meO.body?.email === 'owner@example.com');

  const visitors = await req('/visitors', { token: owner!.token });
  // 22 base visitors + 4 walk-in mirrors (Lara/Sanjay pending, Maya arrived, Spam rejected) = 26
  check('GET /visitors returns 26 seeded visitors (22 base + 4 walk-in mirrors)', visitors.status === 200 && Array.isArray(visitors.body) && visitors.body.length === 26, `count=${visitors.body?.length}`);
  check('Visitors include reasonForVisit', !!visitors.body?.[0]?.reasonForVisit);
  check('Walk-in visitors have visitorRequest relation flag', visitors.body?.some((v: any) => v.visitorRequest));

  const counts = await req('/visitors/counts', { token: owner!.token });
  check('GET /visitors/counts shape', counts.status === 200 && typeof counts.body?.awaitingApproval === 'number');
  // 2 owner/alice pre-registered + 7 new alice-routed pending + 2 pending walk-ins = 11
  check('awaitingApproval = 11', counts.body?.awaitingApproval === 11, `got ${counts.body?.awaitingApproval}`);
  check('walkInPending = 2', counts.body?.walkInPending === 2, `got ${counts.body?.walkInPending}`);

  const approversList = await req('/visitors/approvers', { token: owner!.token });
  check('GET /visitors/approvers returns 3', approversList.status === 200 && approversList.body?.length === 3, `count=${approversList.body?.length}`);

  const checkpoints = await req('/visitors/checkpoints', { token: owner!.token });
  check('GET /visitors/checkpoints returns 2', checkpoints.status === 200 && checkpoints.body?.length === 2);

  const ownerRequests = await req('/visitors/requests', { token: owner!.token });
  check('Owner sees all 4 walk-in requests', ownerRequests.status === 200 && ownerRequests.body?.length === 4, `count=${ownerRequests.body?.length}`);

  // Create + edit
  const createBlank = await req('/visitors', { method: 'POST', token: owner!.token, body: { name: 'No Reason', email: 'nr@x.com' } });
  check('POST /visitors w/o reasonForVisit → 400', createBlank.status === 400, `status=${createBlank.status}`);

  const created = await req('/visitors', {
    method: 'POST', token: owner!.token,
    body: { name: 'Smoke Test User', email: 'smoke@x.com', mobile: '+91 99999 00000', reasonForVisit: 'API smoke test' },
  });
  check('POST /visitors with reasonForVisit → 201', created.status === 201, `status=${created.status}`);
  check('Created visitor has shortId + QR', !!created.body?.shortId && !!created.body?.qrCodeUrl);
  const createdId: string = created.body?.id;

  const updated = await req(`/visitors/${createdId}`, {
    method: 'PUT', token: owner!.token,
    body: { name: 'Smoke Test User (edited)', reasonForVisit: 'Updated reason' },
  });
  check('PUT /visitors/:id updates fields', updated.status === 200 && updated.body?.name === 'Smoke Test User (edited)' && updated.body?.reasonForVisit === 'Updated reason');

  const updateEmpty = await req(`/visitors/${createdId}`, {
    method: 'PUT', token: owner!.token, body: { reasonForVisit: '   ' },
  });
  check('PUT /visitors/:id rejects empty reasonForVisit', updateEmpty.status === 400);

  // ───────── Approver Alice (full perms) ─────────────────────────────────
  section('Approver — Alice (canAddVisitors=true)');

  const meA = await req('/approver/me', { token: alice!.token });
  check('GET /approver/me → canAddVisitors=true', meA.status === 200 && meA.body?.canAddVisitors === true);

  const aliceVisitors = await req('/approver/visitors', { token: alice!.token });
  check('Alice sees a scoped subset of visitors', aliceVisitors.status === 200 && Array.isArray(aliceVisitors.body), `count=${aliceVisitors.body?.length}`);
  const allHerScope = (aliceVisitors.body || []).every((v: any) =>
    v.assignedApproverId === meA.body.id || v.createdByApproverId === meA.body.id
  );
  check('All Alice-visible visitors are assigned-to OR created-by her', allHerScope);
  const aliceVisitorsCount: number = aliceVisitors.body?.length || 0;
  check('Alice scope < owner total (15)', aliceVisitorsCount < 15, `${aliceVisitorsCount} < 15`);

  const aliceRequests = await req('/approver/requests', { token: alice!.token });
  check('Alice sees walk-in requests routed to her', aliceRequests.status === 200 && aliceRequests.body?.length >= 1, `count=${aliceRequests.body?.length}`);

  const aliceCreate = await req('/approver/visitors', {
    method: 'POST', token: alice!.token,
    body: { name: 'Alice-added Smoke', reasonForVisit: 'Routine sales meet', requiresApproval: true },
  });
  check('Alice can create visitor (canAdd=true)', aliceCreate.status === 201);
  check('Self-assigned to Alice when requiresApproval=true', aliceCreate.body?.assignedApproverId === meA.body.id);
  check('Created-by stamp = Alice', aliceCreate.body?.createdByApproverId === meA.body.id);

  const aliceCreateNoReason = await req('/approver/visitors', {
    method: 'POST', token: alice!.token, body: { name: 'No Reason' },
  });
  check('Alice POST without reasonForVisit → 400', aliceCreateNoReason.status === 400);

  // ───────── Approver Bob (canAddVisitors=false) ──────────────────────────
  section('Approver — Bob (canAddVisitors=false)');

  const meB = await req('/approver/me', { token: bob!.token });
  check('Bob /approver/me canAddVisitors=false', meB.body?.canAddVisitors === false);

  const bobCreate = await req('/approver/visitors', {
    method: 'POST', token: bob!.token,
    body: { name: 'Bob Tries', reasonForVisit: 'should fail' },
  });
  check('Bob POST /approver/visitors → 403', bobCreate.status === 403, `status=${bobCreate.status}`);

  // ───────── Scanner — Main Reception checkpoint ─────────────────────────
  section('Reception scanner');

  const badLogin = await req('/visitor-scanner/login', { method: 'POST', body: { mobile: '9000000001', password: 'wrong' } });
  check('Bad scanner password → 401', badLogin.status === 401);

  const sc = await scannerLogin('9000000001', 'scan123');
  if (!check('Scanner login OK', !!sc)) process.exit(1);

  const stats = await req('/visitor-scanner/stats', { token: sc!.token });
  check('GET /stats has expected/arrived/total', stats.status === 200 && 'arrived' in stats.body);

  const scApprovers = await req('/visitor-scanner/approvers', { token: sc!.token });
  // Only active approvers are exposed — Carol (inactive) should be excluded.
  check('Scanner approver list = 2 active', scApprovers.body?.length === 2, `count=${scApprovers.body?.length}`);
  check('Scanner approver list excludes Carol', !scApprovers.body?.some((a: any) => a.email === 'carol@example.com'));

  // Find a fresh EXPECTED no-approval visitor (Priya) and check her in
  const priya = (visitors.body || []).find((v: any) => v.name === 'Priya Shah');
  check('Priya exists in seed', !!priya);
  if (priya) {
    const checkin = await req('/visitor-scanner/checkin', {
      method: 'POST', token: sc!.token, body: { shortId: priya.shortId },
    });
    check('Check-in Priya → ARRIVED', checkin.status === 200 && checkin.body?.status === 'ARRIVED');

    const recheck = await req('/visitor-scanner/checkin', {
      method: 'POST', token: sc!.token, body: { shortId: priya.shortId },
    });
    // Re-scan on an ARRIVED non-frequent visitor returns 200 + alreadyArrived
    // so the scanner UI can show "already in the building" without raising
    // an error. (The legacy expectation was a 400 here.)
    check('Second check-in returns 200 + alreadyArrived',
      recheck.status === 200 && recheck.body?.alreadyArrived === true);
  }

  // Expired visitor (Vikram)
  const vikram = (visitors.body || []).find((v: any) => v.name === 'Vikram Singh');
  if (vikram) {
    const r = await req('/visitor-scanner/checkin', {
      method: 'POST', token: sc!.token, body: { shortId: vikram.shortId },
    });
    check('Expired visitor → 403', r.status === 403, `status=${r.status}`);
  }

  // Approval-required preview flow (Rohan — assigned to Alice)
  const rohan = (visitors.body || []).find((v: any) => v.name === 'Rohan Patel');
  if (rohan) {
    const preview = await req('/visitor-scanner/checkin', {
      method: 'POST', token: sc!.token, body: { shortId: rohan.shortId },
    });
    check('Approval-required preview → 200 + approvalNeeded', preview.status === 200 && preview.body?.approvalNeeded === true);

    const commit = await req('/visitor-scanner/checkin', {
      method: 'POST', token: sc!.token, body: { shortId: rohan.shortId, confirmApprovalRequest: true },
    });
    check('Confirm approval request → 202 AWAITING_APPROVAL', commit.status === 202 && commit.body?.status === 'AWAITING_APPROVAL');

    // Approver Alice approves
    const aliceMe = await req('/approver/me', { token: alice!.token });
    const approve = await req(`/approver/visitors/${rohan.id}/approve`, {
      method: 'POST', token: alice!.token, body: { approvalNote: 'Smoke approve' },
    });
    check('Alice approves Rohan → ARRIVED', approve.status === 200 && approve.body?.status === 'ARRIVED', `approverId=${aliceMe.body.id}`);
  }

  // Walk-in arrived
  const walkBlank = await req('/visitor-scanner/walk-in-arrived', {
    method: 'POST', token: sc!.token, body: { name: 'Walk Smoke' },
  });
  check('walk-in-arrived w/o reasonForVisit → 400', walkBlank.status === 400);

  const walkOk = await req('/visitor-scanner/walk-in-arrived', {
    method: 'POST', token: sc!.token,
    body: { name: 'Walk Smoke', reasonForVisit: 'Courier drop' },
  });
  check('walk-in-arrived with reason → 201 + ARRIVED', walkOk.status === 201 && walkOk.body?.status === 'ARRIVED');

  // Walk-in request
  const reqBlank = await req('/visitor-scanner/requests', {
    method: 'POST', token: sc!.token, body: { name: 'Req Smoke' },
  });
  check('POST /requests w/o reason → 400', reqBlank.status === 400);

  const aliceMe2 = await req('/approver/me', { token: alice!.token });
  const reqOk = await req('/visitor-scanner/requests', {
    method: 'POST', token: sc!.token,
    body: { name: 'Routed Walk-in', reason: 'Wants to meet Alice', assignedApproverId: aliceMe2.body.id },
  });
  check('POST /requests routes to Alice → 201', reqOk.status === 201 && reqOk.body?.assignedApproverId === aliceMe2.body.id);

  // Walk-in linkage: approving Sanjay's request should flip the LINKED Visitor
  // (created with the request) to ARRIVED — no new visitor row.
  const sanjayBefore = (visitors.body || []).find((v: any) => v.name === 'Drop-in: Sanjay Rao');
  check('Sanjay walk-in mirrored as Visitor (AWAITING_APPROVAL)', !!sanjayBefore && sanjayBefore.status === 'AWAITING_APPROVAL');
  const aliceMe3 = await req('/approver/me', { token: alice!.token });
  void aliceMe3;
  const sanjayReq = (await req('/approver/requests', { token: alice!.token })).body?.find((r: any) => /Sanjay/.test(r.name));
  if (sanjayReq) {
    const approveSanjay = await req(`/approver/requests/${sanjayReq.id}/approve`, {
      method: 'POST', token: alice!.token, body: { ownerNote: 'Send up' },
    });
    check('Approve Sanjay → response includes linked visitor', !!approveSanjay.body?.visitor);
    check('Linked visitor flipped to ARRIVED (no new row)', approveSanjay.body?.visitor?.id === sanjayBefore?.id && approveSanjay.body?.visitor?.status === 'ARRIVED');
  }

  const scanHistory = await req('/visitor-scanner/scan-history', { token: sc!.token });
  check('GET /scan-history returns logs (>=5)', scanHistory.status === 200 && scanHistory.body?.length >= 5, `count=${scanHistory.body?.length}`);
  check('Scan history rows include visitor.reasonForVisit', scanHistory.body?.some((s: any) => s.visitor?.reasonForVisit));

  // ───────── Summary ─────────────────────────────────────────────────────
  console.log(`\n${c.cyan('─────────────────────────────────────────────────────────────')}`);
  console.log(`${c.cyan('Smoke complete:')} ${c.green(passed + ' passed')}, ${failed ? c.red(failed + ' failed') : '0 failed'}`);
  if (failures.length) {
    console.log(c.red('\nFailures:'));
    failures.forEach((f) => console.log(`  • ${f}`));
  }
  process.exit(failed > 0 ? 1 : 0);
}

main().catch((err) => { console.error(err); process.exit(2); });
