/**
 * FR-SMS4 RBAC leak regression suite.
 *
 * Closes the four access-control leaks that the audit (`evidence/audit-2026-07-28.md`)
 * identified in dev @ 7fb4f00:
 *
 *   1. GET /api/fees/students        — `?student_id=` ignored for student/parent
 *   2. GET /api/fees/invoices/:uid   — owner/linked-parent/finance allowlist only
 *   3. GET /api/payments/*           — ownership on status / student list / cancel / initiate
 *   4. GET /api/students/:uid/fees   — student self-access when uid matches
 *
 * Tests use the seeded dev DB (see `./setup.js`) and the demo accounts from
 * `.harness/AGENTS.md`. The suite self-installs the parent_students link (the
 * upstream seed has a pre-existing CHECK-constraint typo on `relationship`),
 * a second student row, and a pair of invoice rows so the assertions are
 * deterministic regardless of seed ordering.
 */

const { pointAtDevDb } = require('./setup');

pointAtDevDb();

const request = require('supertest');
const Database = require('better-sqlite3');
const bcrypt = require('bcryptjs');
const { v4: uuidv4 } = require('uuid');

const app = require('../src/index');

async function loginToken(email, password) {
  const r = await request(app).post('/api/auth/login').send({ email, password });
  if (r.status !== 200 || !r.body?.token) {
    throw new Error(`login ${email} -> ${r.status}: ${JSON.stringify(r.body)}`);
  }
  return r.body.token;
}

describe('FR-SMS4 RBAC leak closure', () => {
  let db;
  let tokens;
  let demoStudentId;
  let demoStudentUid;
  let otherStudentId;
  let otherStudentUid;
  let demoInvoiceUid;
  let otherInvoiceUid;

  beforeAll(async () => {
    db = new Database(process.env.DB_PATH);
    db.pragma('foreign_keys = ON');

    tokens = {
      admin:   await loginToken('admin@school.com',   'admin123'),
      teacher: await loginToken('teacher@school.com', 'teacher123'),
      student: await loginToken('student@school.com', 'student123'),
      parent:  await loginToken('parent@school.com',  'parent123'),
      bursar:  await loginToken('bursar@school.com',  'bursar123'),
    };

    const demoStudent = db.prepare(
      "SELECT id, uid FROM users WHERE email='student@school.com'"
    ).get();
    const demoParent = db.prepare(
      "SELECT id FROM users WHERE email='parent@school.com'"
    ).get();
    if (!demoStudent || !demoParent) throw new Error('demo users missing');
    demoStudentId = demoStudent.id;
    demoStudentUid = demoStudent.uid;

    // Repair the parent_students link. The current seed passes
    // relationship='Father' (capital F) but the CHECK constraint enforces
    // lowercase. The link is a fixture for this suite, not behavior under
    // test, so we fix it inline rather than mutate the upstream seed.
    db.prepare(
      `INSERT OR IGNORE INTO parent_students (uid, parent_id, student_id, relationship)
       VALUES (?, ?, ?, 'father')`
    ).run(uuidv4(), demoParent.id, demoStudentId);

    // Second student (cross-account assertions need a victim).
    otherStudentUid = 'uid-rbac-other-student';
    const existingOther = db.prepare('SELECT id FROM users WHERE uid = ?').get(otherStudentUid);
    if (existingOther) {
      otherStudentId = existingOther.id;
    } else {
      const hash = bcrypt.hashSync('student123', 4);
      const r = db.prepare(
        `INSERT INTO users (uid, email, password, first_name, last_name, role, is_active)
         VALUES (?, ?, ?, ?, ?, 'student', 1)`
      ).run(otherStudentUid, 'rbac-other@school.com', hash, 'Other', 'Student');
      otherStudentId = r.lastInsertRowid;
    }

    // Pair of invoices (table is empty after init.js).
    demoInvoiceUid  = 'uid-rbac-invoice-demo';
    otherInvoiceUid = 'uid-rbac-invoice-other';
    const insertInvoice = db.prepare(`
      INSERT OR IGNORE INTO invoices
        (uid, invoice_number, student_id, subtotal, total, paid_amount, status, issued_at, due_date, created_by)
      VALUES (?, ?, ?, 0, 0, 0, 'issued', datetime('now'), date('now'), ?)
    `);
    insertInvoice.run(demoInvoiceUid,  'INV-RBAC-DEMO-001',  demoStudentId,  demoStudentId);
    insertInvoice.run(otherInvoiceUid, 'INV-RBAC-OTHER-001', otherStudentId, demoStudentId);

    // student_fee for the other student (used by initiate + cancel tests).
    db.prepare(
      `INSERT OR IGNORE INTO student_fees (uid, student_id, fee_group_id, amount, status, due_date)
       VALUES (?, ?, 1, 100, 'pending', date('now'))`
    ).run('uid-rbac-sfee-other', otherStudentId);
    // Reset a pending payment for the other student to keep the cancel test
    // deterministic across multiple test runs against the same DB.
    db.prepare('DELETE FROM payments WHERE uid = ?').run('uid-rbac-pay-other');
    db.prepare(
      `INSERT INTO payments (uid, student_fee_id, amount, payment_method, reference_number, collected_by, status)
       VALUES (?, (SELECT id FROM student_fees WHERE uid = 'uid-rbac-sfee-other'), 50, 'cash', 'REF-RBAC-OTHER-001', ?, 'pending')`
    ).run('uid-rbac-pay-other', demoStudentId);

    // Fresh student_fee for the demo student so the "own initiate" test has
    // a deterministic owner check (paid_amount=0, amount=100, status='pending').
    // The seeded student_fees.id=1 may have its paid_amount bumped by prior
    // webhook test runs, which would 400 the initiate route with
    // "Fee already fully paid" instead of exercising the ownership gate.
    // Use INSERT OR IGNORE — nothing in this suite mutates paid_amount, so
    // the row stays deterministic across multiple runs.
    db.prepare(
      `INSERT OR IGNORE INTO student_fees (uid, student_id, fee_group_id, amount, paid_amount, status, due_date)
       VALUES (?, ?, 1, 100, 0, 'pending', date('now'))`
    ).run('uid-rbac-sfee-demo-fresh', demoStudentId);
  });

  afterAll(() => { db.close(); });

  // ===================== Leak 1 =====================

  describe('GET /api/fees/students', () => {
    it('student sees only own rows even when student_id overrides to another id', async () => {
      const res = await request(app)
        .get(`/api/fees/students?student_id=${otherStudentId}`)
        .set('Authorization', `Bearer ${tokens.student}`);
      expect(res.status).toBe(200);
      expect(Array.isArray(res.body)).toBe(true);
      expect(res.body.every((r) => r.student_id === demoStudentId)).toBe(true);
    });

    it('parent ignores student_id override and returns own children only', async () => {
      const res = await request(app)
        .get(`/api/fees/students?student_id=${otherStudentId}`)
        .set('Authorization', `Bearer ${tokens.parent}`);
      expect(res.status).toBe(200);
      expect(Array.isArray(res.body)).toBe(true);
      // parent is linked to demoStudent only — the requested unlinked id must not appear
      expect(res.body.every((r) => r.student_id !== otherStudentId)).toBe(true);
    });

    it('bursar can still pass an arbitrary student_id', async () => {
      const res = await request(app)
        .get(`/api/fees/students?student_id=${demoStudentId}`)
        .set('Authorization', `Bearer ${tokens.bursar}`);
      expect(res.status).toBe(200);
      expect(res.body.some((r) => r.student_id === demoStudentId)).toBe(true);
    });
  });

  // ===================== Leak 2 =====================

  describe('GET /api/fees/invoices/:uid', () => {
    it('bursar (finance role) can read any invoice', async () => {
      const res = await request(app)
        .get(`/api/fees/invoices/${demoInvoiceUid}`)
        .set('Authorization', `Bearer ${tokens.bursar}`);
      expect(res.status).toBe(200);
    });

    it('teacher is forbidden from reading any invoice', async () => {
      const res = await request(app)
        .get(`/api/fees/invoices/${demoInvoiceUid}`)
        .set('Authorization', `Bearer ${tokens.teacher}`);
      expect(res.status).toBe(403);
    });

    it('student is allowed to read their own invoice (owner)', async () => {
      const res = await request(app)
        .get(`/api/fees/invoices/${demoInvoiceUid}`)
        .set('Authorization', `Bearer ${tokens.student}`);
      expect(res.status).toBe(200);
    });

    it('student is forbidden from reading another student\'s invoice', async () => {
      const res = await request(app)
        .get(`/api/fees/invoices/${otherInvoiceUid}`)
        .set('Authorization', `Bearer ${tokens.student}`);
      expect(res.status).toBe(403);
    });

    it('parent can read a linked child\'s invoice', async () => {
      const res = await request(app)
        .get(`/api/fees/invoices/${demoInvoiceUid}`)
        .set('Authorization', `Bearer ${tokens.parent}`);
      expect(res.status).toBe(200);
    });

    it('parent is forbidden from reading an unlinked child\'s invoice', async () => {
      const res = await request(app)
        .get(`/api/fees/invoices/${otherInvoiceUid}`)
        .set('Authorization', `Bearer ${tokens.parent}`);
      expect(res.status).toBe(403);
    });
  });

  // ===================== Leak 3 =====================

  describe('GET /api/payments/student/:studentId', () => {
    it('student is forbidden from fetching another student\'s payment list', async () => {
      const res = await request(app)
        .get(`/api/payments/student/${otherStudentId}`)
        .set('Authorization', `Bearer ${tokens.student}`);
      expect(res.status).toBe(403);
    });

    it('student can fetch their own payment list', async () => {
      const res = await request(app)
        .get(`/api/payments/student/${demoStudentId}`)
        .set('Authorization', `Bearer ${tokens.student}`);
      expect(res.status).toBe(200);
    });

    it('bursar can fetch any student\'s payment list', async () => {
      const res = await request(app)
        .get(`/api/payments/student/${demoStudentId}`)
        .set('Authorization', `Bearer ${tokens.bursar}`);
      expect(res.status).toBe(200);
    });
  });

  describe('GET /api/payments/status/:paymentId', () => {
    it('student can read status of their own payment', async () => {
      const res = await request(app)
        .get('/api/payments/status/uid-pay-1')
        .set('Authorization', `Bearer ${tokens.student}`);
      expect(res.status).toBe(200);
    });

    it('teacher cannot read status of any payment', async () => {
      const res = await request(app)
        .get('/api/payments/status/uid-pay-1')
        .set('Authorization', `Bearer ${tokens.teacher}`);
      expect(res.status).toBe(403);
    });
  });

  describe('POST /api/payments/initiate', () => {
    it('student can initiate against their own student_fee', async () => {
      const ownFee = db.prepare(
        "SELECT id FROM student_fees WHERE uid='uid-rbac-sfee-demo-fresh'"
      ).get();
      const res = await request(app)
        .post('/api/payments/initiate')
        .set('Authorization', `Bearer ${tokens.student}`)
        .send({ student_fee_id: ownFee.id, amount: 10 });
      expect(res.status).toBe(200);
      expect(res.body.payment_id).toBeTruthy();
    });

    it('student cannot initiate against another student\'s student_fee', async () => {
      const otherFee = db.prepare(
        "SELECT id FROM student_fees WHERE uid='uid-rbac-sfee-other'"
      ).get();
      const res = await request(app)
        .post('/api/payments/initiate')
        .set('Authorization', `Bearer ${tokens.student}`)
        .send({ student_fee_id: otherFee.id, amount: 10 });
      expect(res.status).toBe(403);
    });
  });

  describe('DELETE /api/payments/:paymentId', () => {
    it('student cannot cancel another student\'s payment', async () => {
      const res = await request(app)
        .delete('/api/payments/uid-rbac-pay-other')
        .set('Authorization', `Bearer ${tokens.student}`);
      expect(res.status).toBe(403);
    });
  });

  // ===================== Leak 4 =====================

  describe('GET /api/students/:uid/fees', () => {
    it('student can access their own UID via the canonical path', async () => {
      const res = await request(app)
        .get(`/api/students/${demoStudentUid}/fees`)
        .set('Authorization', `Bearer ${tokens.student}`);
      expect(res.status).toBe(200);
    });

    it('student cannot access another student\'s UID', async () => {
      const res = await request(app)
        .get(`/api/students/${otherStudentUid}/fees`)
        .set('Authorization', `Bearer ${tokens.student}`);
      expect(res.status).toBe(403);
    });

    it('parent can access a linked child\'s UID', async () => {
      const res = await request(app)
        .get(`/api/students/${demoStudentUid}/fees`)
        .set('Authorization', `Bearer ${tokens.parent}`);
      expect(res.status).toBe(200);
    });
  });
});