// Tests for FR-SMS4 transactional payment surface.
//
// Verifies:
//   1. POST /api/fees/pay records a manual payment, bumps fee.paid_amount,
//      transitions status to 'partial' (or 'paid' on full settlement),
//      and is atomic across fee + linked invoice.
//   2-4. Validation + RBAC of /api/fees/pay.
//   5. Paynow webhook idempotency: two identical calls produce a single
//      student_fees.paid_amount increment.
//   6. Paynow 'Awaiting' webhook does NOT touch student_fees / invoices.
//   7. Paynow 'Paid' webhook updates student_fees AND the linked invoice
//      in the same transaction.
//
// Each test creates its own isolated user + fee_group + student_fee (and
// invoice / payment where relevant) so we don't race with other suites
// that share the dev DB.

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

pointAtDevDb();
const request = require('supertest');
const app = require('../src/index');
const Database = require('better-sqlite3');
const { v4: uuidv4 } = require('uuid');

// -------- Per-test fixture helpers ---------------------------------------

function openDb() {
  const db = new Database(process.env.DB_PATH);
  db.pragma('foreign_keys = ON');
  return db;
}

function createStudent(db) {
  const uid = uuidv4();
  const email = `paytest-student-${uid}@school.com`;
  const r = db.prepare(`
    INSERT INTO users (uid, email, password, role, first_name, last_name, is_active)
    VALUES (?, ?, 'hash', 'student', 'Pay', 'Test', 1)
  `).run(uid, email);
  return { id: r.lastInsertRowid, uid, email };
}

function createFeeGroup(db, name = 'Test Tuition', amount = 500) {
  const uid = uuidv4();
  const r = db.prepare(`
    INSERT INTO fee_groups (uid, name, amount, type, frequency, academic_year)
    VALUES (?, ?, ?, 'tuition', 'termly', '2026')
  `).run(uid, name, amount);
  return { id: r.lastInsertRowid, uid };
}

function createStudentFee(db, { student_id, fee_group_id, amount = 500, paid_amount = 0, status = 'pending' }) {
  const uid = uuidv4();
  const r = db.prepare(`
    INSERT INTO student_fees (uid, student_id, fee_group_id, amount, paid_amount, status, academic_year)
    VALUES (?, ?, ?, ?, ?, ?, '2026')
  `).run(uid, student_id, fee_group_id, amount, paid_amount, status);
  return { id: r.lastInsertRowid, uid };
}

function createInvoice(db, { student_id, fee_group_id, total = 500, paid_amount = 0, status = 'issued' }) {
  const uid = uuidv4();
  const invoiceNumber = `INV-${Date.now()}-${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
  const r = db.prepare(`
    INSERT INTO invoices (uid, invoice_number, student_id, fee_group_id, total, paid_amount, status, issued_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now','localtime'))
  `).run(uid, invoiceNumber, student_id, fee_group_id, total, paid_amount, status);
  return { id: r.lastInsertRowid, uid };
}

function createPendingPayment(db, { student_fee_id, amount, reference, payment_method = 'paynow' }) {
  const uid = uuidv4();
  db.prepare(`
    INSERT INTO payments (uid, student_fee_id, amount, payment_method, reference_number, status, collected_by)
    VALUES (?, ?, ?, ?, ?, 'pending', 1)
  `).run(uid, student_fee_id, amount, payment_method, reference);
  return { uid, reference };
}

// -------- Suite ----------------------------------------------------------

describe('FR-SMS4 transactional payment surface', () => {
  let adminToken;
  let studentToken;

  beforeAll(async () => {
    const r1 = await request(app)
      .post('/api/auth/login')
      .send({ email: 'admin@school.com', password: 'admin123' });
    adminToken = r1.body?.token;
    const r2 = await request(app)
      .post('/api/auth/login')
      .send({ email: 'student@school.com', password: 'student123' });
    studentToken = r2.body?.token;
  });

  // -------- Task B-1: /api/fees/pay ----------------------------------------

  describe('POST /api/fees/pay (manual payment recording)', () => {
    it('1. records a partial payment, updates paid_amount + status', async () => {
      const db = openDb();
      try {
        const student = createStudent(db);
        const group = createFeeGroup(db, 'Partial Tuition', 500);
        const fee = createStudentFee(db, {
          student_id: student.id, fee_group_id: group.id,
          amount: 500, paid_amount: 0, status: 'pending',
        });

        const res = await request(app)
          .post('/api/fees/pay')
          .set('Authorization', `Bearer ${adminToken}`)
          .send({ student_fee_id: fee.id, amount: 200, method: 'cash', reference: 'CASH-TEST-001' });

        expect(res.status).toBe(201);
        expect(res.body.success).toBe(true);
        expect(res.body.payment_id).toMatch(/^[0-9a-f-]{36}$/);
        expect(res.body.student_fee_id).toBe(fee.id);
        expect(res.body.new_paid_amount).toBe(200);
        expect(res.body.outstanding_after).toBe(300);
        expect(res.body.invoice_updated).toBeNull();

        // DB row matches.
        const row = db.prepare('SELECT paid_amount, status, sync_status FROM student_fees WHERE id = ?').get(fee.id);
        expect(row.paid_amount).toBe(200);
        expect(row.status).toBe('partial');
        expect(row.sync_status).toBe('pending');

        // Payment row exists.
        const pay = db.prepare("SELECT * FROM payments WHERE uid = ?").get(res.body.payment_id);
        expect(pay).toBeTruthy();
        expect(pay.amount).toBe(200);
        expect(pay.status).toBe('completed');
        expect(pay.reference_number).toBe('CASH-TEST-001');
      } finally {
        db.close();
      }
    });

    it('settles to status=paid when amount covers the full outstanding', async () => {
      const db = openDb();
      try {
        const student = createStudent(db);
        const group = createFeeGroup(db, 'Full Settle', 200);
        const fee = createStudentFee(db, {
          student_id: student.id, fee_group_id: group.id,
          amount: 200, paid_amount: 0, status: 'pending',
        });

        const res = await request(app)
          .post('/api/fees/pay')
          .set('Authorization', `Bearer ${adminToken}`)
          .send({ student_fee_id: fee.id, amount: 200, method: 'bank' });

        expect(res.status).toBe(201);
        expect(res.body.new_paid_amount).toBe(200);
        expect(res.body.outstanding_after).toBe(0);

        const row = db.prepare('SELECT paid_amount, status, paid_at FROM student_fees WHERE id = ?').get(fee.id);
        expect(row.status).toBe('paid');
        expect(row.paid_at).toBeTruthy();
      } finally {
        db.close();
      }
    });

    it('also updates the linked invoice atomically (same fee_group + student)', async () => {
      const db = openDb();
      try {
        const student = createStudent(db);
        const group = createFeeGroup(db, 'Linked Inv', 500);
        const fee = createStudentFee(db, {
          student_id: student.id, fee_group_id: group.id,
          amount: 500, paid_amount: 0, status: 'pending',
        });
        const invoice = createInvoice(db, {
          student_id: student.id, fee_group_id: group.id,
          total: 500, paid_amount: 0, status: 'issued',
        });

        const res = await request(app)
          .post('/api/fees/pay')
          .set('Authorization', `Bearer ${adminToken}`)
          .send({ student_fee_id: fee.id, amount: 150 });

        expect(res.status).toBe(201);
        expect(res.body.invoice_updated).toBe(invoice.uid);

        const inv = db.prepare('SELECT paid_amount, status FROM invoices WHERE uid = ?').get(invoice.uid);
        expect(inv.paid_amount).toBe(150);
        expect(inv.status).toBe('partial');
      } finally {
        db.close();
      }
    });

    it('2. rejects overpay with 400', async () => {
      const db = openDb();
      try {
        const student = createStudent(db);
        const group = createFeeGroup(db, 'Overpay', 100);
        const fee = createStudentFee(db, {
          student_id: student.id, fee_group_id: group.id,
          amount: 100, paid_amount: 0, status: 'pending',
        });

        const res = await request(app)
          .post('/api/fees/pay')
          .set('Authorization', `Bearer ${adminToken}`)
          .send({ student_fee_id: fee.id, amount: 5000 });

        expect(res.status).toBe(400);
        expect(res.body.error).toMatch(/exceeds outstanding/i);

        // DB untouched.
        const row = db.prepare('SELECT paid_amount, status FROM student_fees WHERE id = ?').get(fee.id);
        expect(row.paid_amount).toBe(0);
        expect(row.status).toBe('pending');
      } finally {
        db.close();
      }
    });

    it('rejects amount <= 0 with 400', async () => {
      const db = openDb();
      try {
        const student = createStudent(db);
        const group = createFeeGroup(db, 'Zero', 100);
        const fee = createStudentFee(db, {
          student_id: student.id, fee_group_id: group.id,
          amount: 100, paid_amount: 0, status: 'pending',
        });

        const res = await request(app)
          .post('/api/fees/pay')
          .set('Authorization', `Bearer ${adminToken}`)
          .send({ student_fee_id: fee.id, amount: 0 });

        expect(res.status).toBe(400);
      } finally {
        db.close();
      }
    });

    it('3. rejects unknown student_fee_id with 404', async () => {
      const res = await request(app)
        .post('/api/fees/pay')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ student_fee_id: 9999999, amount: 10 });

      expect(res.status).toBe(404);
      expect(res.body.error).toMatch(/not found/i);
    });

    it('4. rejects student role with 403 (RBAC)', async () => {
      const db = openDb();
      try {
        const student = createStudent(db);
        const group = createFeeGroup(db, 'RBAC', 100);
        const fee = createStudentFee(db, {
          student_id: student.id, fee_group_id: group.id,
          amount: 100, paid_amount: 0, status: 'pending',
        });

        const res = await request(app)
          .post('/api/fees/pay')
          .set('Authorization', `Bearer ${studentToken}`)
          .send({ student_fee_id: fee.id, amount: 50 });

        expect(res.status).toBe(403);

        // DB untouched.
        const row = db.prepare('SELECT paid_amount, status FROM student_fees WHERE id = ?').get(fee.id);
        expect(row.paid_amount).toBe(0);
        expect(row.status).toBe('pending');
      } finally {
        db.close();
      }
    });

    it('rejects unauthenticated request with 401', async () => {
      const res = await request(app)
        .post('/api/fees/pay')
        .send({ student_fee_id: 1, amount: 10 });
      expect(res.status).toBe(401);
    });
  });

  // -------- Task B-2: webhook transactions --------------------------------

  describe('POST /api/payments/webhook (transactional)', () => {
    it('5. is idempotent — replay does not double-credit', async () => {
      const db = openDb();
      try {
        const student = createStudent(db);
        const group = createFeeGroup(db, 'Webhook Idem', 500);
        const fee = createStudentFee(db, {
          student_id: student.id, fee_group_id: group.id,
          amount: 500, paid_amount: 0, status: 'pending',
        });
        const ref = `IDEMP-${uuidv4().slice(0, 8)}`;
        const { uid: payUid } = createPendingPayment(db, {
          student_fee_id: fee.id, amount: 100, reference: ref,
        });

        const r1 = await request(app)
          .post('/api/payments/webhook')
          .set('Content-Type', 'application/json')
          .send({ reference: ref, status: 'Paid', amount: 100 });
        const r2 = await request(app)
          .post('/api/payments/webhook')
          .set('Content-Type', 'application/json')
          .send({ reference: ref, status: 'Paid', amount: 100 });

        expect(r1.status).toBeLessThan(500);
        expect(r2.status).toBeLessThan(500);
        // Second response includes the 'already processed' note.
        expect(r2.body.note).toBe('already processed');

        // Exactly ONE increment: 0 + 100 = 100, not 0 + 100 + 100 = 200.
        const feeRow = db.prepare('SELECT paid_amount, status FROM student_fees WHERE id = ?').get(fee.id);
        expect(feeRow.paid_amount).toBe(100);
        expect(feeRow.status).toBe('partial');

        // Only one payment row for this reference.
        const count = db.prepare("SELECT COUNT(*) as c FROM payments WHERE reference_number = ?").get(ref).c;
        expect(count).toBe(1);

        // Payment row is completed.
        const pay = db.prepare('SELECT status FROM payments WHERE uid = ?').get(payUid);
        expect(pay.status).toBe('completed');
      } finally {
        db.close();
      }
    });

    it('6. Awaiting status does NOT touch student_fees / invoices', async () => {
      const db = openDb();
      try {
        const student = createStudent(db);
        const group = createFeeGroup(db, 'Webhook Awaiting', 300);
        const fee = createStudentFee(db, {
          student_id: student.id, fee_group_id: group.id,
          amount: 300, paid_amount: 0, status: 'pending',
        });
        const invoice = createInvoice(db, {
          student_id: student.id, fee_group_id: group.id,
          total: 300, paid_amount: 0, status: 'issued',
        });
        const ref = `AWAIT-${uuidv4().slice(0, 8)}`;
        createPendingPayment(db, { student_fee_id: fee.id, amount: 50, reference: ref });

        const res = await request(app)
          .post('/api/payments/webhook')
          .set('Content-Type', 'application/json')
          .send({ reference: ref, status: 'Awaiting', amount: 50 });
        expect(res.status).toBeLessThan(500);

        // student_fees untouched.
        const feeRow = db.prepare('SELECT paid_amount, status FROM student_fees WHERE id = ?').get(fee.id);
        expect(feeRow.paid_amount).toBe(0);
        expect(feeRow.status).toBe('pending');
        // Invoice untouched.
        const invRow = db.prepare('SELECT paid_amount, status FROM invoices WHERE uid = ?').get(invoice.uid);
        expect(invRow.paid_amount).toBe(0);
        expect(invRow.status).toBe('issued');
        // Payment row is pending (not completed).
        const pay = db.prepare('SELECT status FROM payments WHERE reference_number = ?').get(ref);
        expect(pay.status).toBe('pending');
      } finally {
        db.close();
      }
    });

    it('7. Paid status updates both student_fees AND linked invoice atomically', async () => {
      const db = openDb();
      try {
        const student = createStudent(db);
        const group = createFeeGroup(db, 'Webhook Inv', 500);
        const fee = createStudentFee(db, {
          student_id: student.id, fee_group_id: group.id,
          amount: 500, paid_amount: 0, status: 'pending',
        });
        const invoice = createInvoice(db, {
          student_id: student.id, fee_group_id: group.id,
          total: 500, paid_amount: 0, status: 'issued',
        });
        const ref = `PAID-INV-${uuidv4().slice(0, 8)}`;
        createPendingPayment(db, { student_fee_id: fee.id, amount: 200, reference: ref });

        const res = await request(app)
          .post('/api/payments/webhook')
          .set('Content-Type', 'application/json')
          .send({ reference: ref, status: 'Paid', amount: 200 });
        expect(res.status).toBeLessThan(500);

        const feeRow = db.prepare('SELECT paid_amount, status FROM student_fees WHERE id = ?').get(fee.id);
        expect(feeRow.paid_amount).toBe(200);
        expect(feeRow.status).toBe('partial');

        const invRow = db.prepare('SELECT paid_amount, status, sync_status FROM invoices WHERE uid = ?').get(invoice.uid);
        expect(invRow.paid_amount).toBe(200);
        expect(invRow.status).toBe('partial');
        expect(invRow.sync_status).toBe('pending');
      } finally {
        db.close();
      }
    });
  });

  // -------- Task B-3: live-mode refusal -----------------------------------

  describe('POST /api/payments/initiate (no creds → test mode refusal)', () => {
    it('returns mode=test with the configured-credentials instructions text', async () => {
      // We deliberately don't touch env vars here — the test-setup sets a
      // blank PAYNOW_INTEGRATION_ID/KEY, and we assert on the live refusal
      // shape. If a developer overrides those env vars locally, this test
      // will exercise the live path and fail; that is the desired signal.
      const db = openDb();
      try {
        const student = createStudent(db);
        const group = createFeeGroup(db, 'Initiate Refusal', 100);
        const fee = createStudentFee(db, {
          student_id: student.id, fee_group_id: group.id,
          amount: 100, paid_amount: 0, status: 'pending',
        });

        const res = await request(app)
          .post('/api/payments/initiate')
          .set('Authorization', `Bearer ${adminToken}`)
          .send({ student_fee_id: fee.id, amount: 50 });

        expect(res.status).toBe(200);
        expect(res.body.success).toBe(true);
        expect(res.body.mode).toBe('test');
        expect(res.body.poll_url).toBeNull();
        expect(res.body.payment_id).toBeTruthy();
        expect(res.body.reference).toBeTruthy();
        expect(res.body.instructions).toBe(
          'Set PAYNOW_INTEGRATION_ID and PAYNOW_INTEGRATION_KEY to enable live payments.',
        );
      } finally {
        db.close();
      }
    });
  });
});