'use strict';

// Debt-payoff math (Snowball review #5 + #1). Pure functions — hand-calculated
// examples, plus the unpayable-debt edge where a minimum never overcomes interest.
const test = require('node:test');
const assert = require('node:assert/strict');

const { calculateSnowball, calculateAvalanche } = require('../services/snowballService');
const {
  monthlyInterest, monthsToPayoff, amortizationSchedule,
  calculateMinimumOnly, debtAprSnapshot,
} = require('../services/aprService');

// ── primitives ───────────────────────────────────────────────────────────────

test('monthlyInterest = balance × APR/12', () => {
  assert.equal(monthlyInterest(1000, 12), 10);   // 1% / month
  assert.equal(monthlyInterest(1000, 0), 0);
  assert.equal(monthlyInterest(-5, 12), 0);      // clamped
});

test('monthsToPayoff — hand examples + edge cases', () => {
  assert.equal(monthsToPayoff(1000, 0, 100), 10);   // no interest: 1000/100
  assert.equal(monthsToPayoff(0, 12, 100), 0);      // already paid
  assert.equal(monthsToPayoff(1000, 12, 0), null);  // no payment
  assert.equal(monthsToPayoff(1000, 12, 5), null);  // 5 < 1000×1% interest → never
  assert.ok(monthsToPayoff(1000, 12, 100) > 10);    // interest adds a few months
});

test('amortizationSchedule — APR=0 is principal-only', () => {
  const s = amortizationSchedule(1000, 0, 100);
  assert.equal(s.length, 10);
  assert.deepEqual(s[0], { month: 1, payment: 100, principal: 100, interest: 0, balance: 900 });
  assert.equal(s[9].balance, 0);
});

test('amortizationSchedule — first row of a 12% APR loan', () => {
  const s = amortizationSchedule(1000, 12, 100);
  // interest = 1000×0.01 = 10; principal = 90; balance = 910
  assert.deepEqual(s[0], { month: 1, payment: 100, principal: 90, interest: 10, balance: 910 });
});

test('amortizationSchedule — unpayable / invalid returns []', () => {
  assert.deepEqual(amortizationSchedule(1000, 12, 5), []);   // payment < interest
  assert.deepEqual(amortizationSchedule(0, 12, 100), []);    // no balance
  assert.deepEqual(amortizationSchedule(1000, 12, 0), []);   // no payment
});

// ── snowball simulation ────────────────────────────────────────────────────────

test('calculateSnowball rolls a freed minimum into the next debt (0% hand example)', () => {
  // A(200/min100) then B(300/min100), no interest, no extra.
  // m1: A→100, B→200 · m2: A→0 (payoff m2, roll +100), B→100 · m3: B→0 (payoff m3)
  const r = calculateSnowball([
    { id: 1, name: 'A', current_balance: 200, minimum_payment: 100, interest_rate: 0 },
    { id: 2, name: 'B', current_balance: 300, minimum_payment: 100, interest_rate: 0 },
  ], 0);
  assert.equal(r.months_to_freedom, 3);
  assert.equal(r.total_interest_paid, 0);
  assert.equal(r.debts.find(d => d.id === 1).payoff_month, 2);
  assert.equal(r.debts.find(d => d.id === 2).payoff_month, 3);
  assert.equal(r.capped, false);
});

test('calculateAvalanche pays less interest than snowball when order matters', () => {
  const debts = [
    { id: 1, name: 'Low APR',  current_balance: 1000, minimum_payment: 100, interest_rate: 5 },
    { id: 2, name: 'High APR', current_balance: 1000, minimum_payment: 100, interest_rate: 30 },
  ];
  const sb = calculateSnowball(debts, 200);   // attacks id 1 first (given order)
  const av = calculateAvalanche(debts, 200);  // reorders → attacks id 2 (30%) first
  assert.ok(av.total_interest_paid <= sb.total_interest_paid, 'avalanche ≤ snowball interest');
  assert.equal(av.months_to_freedom != null, true);
});

test('UNPAYABLE debt: minimum below interest → months_to_freedom null, capped, never_paid (#1)', () => {
  // 1000 @ 24% APR = 20/mo interest, min 10 → balance grows forever.
  const r = calculateSnowball([
    { id: 1, name: 'Stuck', current_balance: 1000, minimum_payment: 10, interest_rate: 24 },
  ], 0);
  assert.equal(r.months_to_freedom, null, 'no honest payoff date');
  assert.equal(r.payoff_date, null);
  assert.equal(r.capped, true);
  assert.equal(r.debts[0].never_paid, true);
});

test('UNPAYABLE debt among payable ones does not fake a freedom date (#1)', () => {
  // The rolled snowball ($50) is far too small to ever cover Stuck's ~$300/mo
  // interest, so Stuck never clears even after Payable is gone.
  const r = calculateSnowball([
    { id: 1, name: 'Payable', current_balance: 50,    minimum_payment: 50, interest_rate: 0 },
    { id: 2, name: 'Stuck',   current_balance: 10000, minimum_payment: 10, interest_rate: 36 },
  ], 0);
  assert.equal(r.months_to_freedom, null, 'one unpayable debt ⇒ not free');
  assert.equal(r.capped, true);
  assert.equal(r.debts.find(d => d.id === 1).payoff_month, 1); // payable one still records its month
  assert.equal(r.debts.find(d => d.id === 2).never_paid, true);
});

test('calculateSnowball skips zero / missing balances', () => {
  const r = calculateSnowball([
    { id: 1, name: 'Zero', current_balance: 0,    minimum_payment: 50, interest_rate: 0 },
    { id: 2, name: 'Null', current_balance: null, minimum_payment: 50, interest_rate: 0 },
    { id: 3, name: 'Real', current_balance: 100,  minimum_payment: 100, interest_rate: 0 },
  ], 0);
  assert.equal(r.debts.length, 1);
  assert.equal(r.skipped.length, 2);
  assert.deepEqual(r.skipped.map(s => s.reason).sort(), ['no_balance', 'zero_balance']);
});

// ── minimum-only baseline ──────────────────────────────────────────────────────

test('calculateMinimumOnly flags its skip reasons', () => {
  const r = calculateMinimumOnly([
    { id: 1, name: 'NoBal',  current_balance: null, minimum_payment: 50, interest_rate: 0 },
    { id: 2, name: 'Stuck',  current_balance: 1000, minimum_payment: 5,  interest_rate: 12 }, // 5 < 10 interest
    { id: 3, name: 'NoMin',  current_balance: 500,  minimum_payment: 0,  interest_rate: 0 },
    { id: 4, name: 'Good',   current_balance: 300,  minimum_payment: 100, interest_rate: 0 },
  ]);
  const reasons = Object.fromEntries(r.skipped.map(s => [s.name, s.reason]));
  assert.equal(reasons.NoBal, 'no_balance');
  assert.equal(reasons.Stuck, 'payment_below_interest');
  assert.equal(reasons.NoMin, 'no_minimum_payment');
  assert.equal(r.debts.length, 1);        // only "Good" is active
  assert.equal(r.months_to_freedom, 3);   // 300 / 100
});

// ── APR snapshot ───────────────────────────────────────────────────────────────

test('debtAprSnapshot breaks down a minimum payment', () => {
  const snap = debtAprSnapshot({ current_balance: 1000, interest_rate: 12, minimum_payment: 50 });
  assert.equal(snap.monthly_interest, 10);        // 1000 × 1%
  assert.equal(snap.principal_per_min_pmt, 40);   // 50 − 10
  assert.equal(snap.interest_pct_of_min, 20);     // 10/50
  assert.equal(snap.annual_interest_estimate, 120);
  assert.equal(debtAprSnapshot({ current_balance: 0 }), null);
});
