import { describe, it, expect } from "vitest";
import {
  buildFinancialState,
  projectScenario,
  standardScenarios,
  scoreDecision,
  detectAlerts,
  nextBestAction,
  intelligenceSummary,
  type ProfileLike,
} from "./intelligence";
import type { TxLike } from "./finance";

const NOW = Date.now();
const iso = (daysAgo: number) => new Date(NOW - daysAgo * 86_400_000).toISOString();
const RATE = 90 / 30.44; // trailing-90-day to monthly conversion

// Steady state: 10,000 baseline + 3,000 income - 1,000 expenses over 90 days.
const profile: ProfileLike = { currency: "INR", monthly_income: 3000, savings: 10000 };
const txs: TxLike[] = [
  { amount: 3000, type: "income", occurred_at: iso(30) },
  { amount: 600, type: "expense", occurred_at: iso(15) },
  { amount: 400, type: "expense", occurred_at: iso(5) },
];
const goals = [
  { id: "g1", name: "House down payment", target_amount: 50000, current_amount: 14000, monthly_contribution: 500 },
];

function baseState() {
  return buildFinancialState(profile, txs, goals, NOW);
}

describe("buildFinancialState", () => {
  const s = baseState();

  it("derives net worth from baseline plus all transactions", () => {
    expect(s.netWorth).toBe(12000);
    expect(s.cash).toBe(12000);
  });

  it("estimates monthly figures from the 90-day window", () => {
    expect(s.monthlyExpenses).toBe(Math.round(1000 / RATE)); // 338
    expect(s.monthlySurplus).toBe(Math.round(2000 / RATE)); // 676
    expect(s.monthlyIncome).toBe(3000);
    expect(s.savingsRate).toBe(23);
  });

  it("computes reserve months and excess cash against the target", () => {
    expect(s.reserveTargetMonths).toBe(6);
    expect(s.reserveTargetAmount).toBe(Math.round((1000 / RATE) * 6)); // 2029
    expect(s.reserveMonths).toBe(35.5);
    expect(s.excessCash).toBe(12000 - Math.round((1000 / RATE) * 6));
  });

  it("projects goals and leaves drift undefined without a deadline", () => {
    const g = s.goals[0];
    expect(g.remaining).toBe(36000);
    expect(g.perMonth).toBe(500);
    expect(g.monthsToGoal).toBe(72);
    expect(g.driftWeeks).toBeNull();
    expect(g.odds).toBeNull();
    expect(s.primaryGoal?.name).toBe("House down payment");
  });

  it("marks an empty ledger as having no data", () => {
    const empty = buildFinancialState(profile, [], goals, NOW);
    expect(empty.hasData).toBe(false);
    expect(empty.netWorth).toBe(10000);
    expect(empty.monthlyExpenses).toBe(0);
  });
});

describe("projectScenario", () => {
  const s = baseState();

  it("subtracts the outlay from cash", () => {
    const o = projectScenario(s, { id: "now", label: "Buy now", amount: 5000 });
    expect(o.cashAfter).toBe(7000);
    expect(o.reserveMonthsAfter).toBe(20.7);
    expect(o.surplusAfter).toBe(676);
  });

  it("lets surplus build up during a wait", () => {
    const o = projectScenario(s, { id: "wait", label: "Wait 3 months", amount: 5000, waitMonths: 3 });
    expect(o.cashAfter).toBe(12000 + 676 * 3 - 5000); // 9028
  });

  it("charges an ongoing monthly cost against surplus and goal rate", () => {
    const o = projectScenario(s, { id: "now", label: "Buy now", amount: 5000, monthlyCost: 200 });
    expect(o.surplusAfter).toBe(476);
    // base 72 months + 10 months lost to the outlay, at 300/month → 130 total
    const base = 72;
    const perMonthAfter = 300;
    const after = Math.ceil(36000 / perMonthAfter) + 5000 / 500;
    expect(o.goalDelayWeeks).toBe(Math.round((after - base) * 4.345)); // 252
  });

  it("delays the primary goal by the outlay expressed in months of contributions", () => {
    const o = projectScenario(s, { id: "now", label: "Buy now", amount: 5000 });
    expect(o.goalDelayWeeks).toBe(Math.round(10 * 4.345)); // 43
    expect(o.goalProjectedDate).not.toBeNull();
  });

  it("the standard four options always include a free skip", () => {
    const specs = standardScenarios(10000, 250);
    expect(specs.map((x) => x.id)).toEqual(["now", "wait", "cheaper", "skip"]);
    expect(specs[2].amount).toBe(6000);
    expect(specs[3].amount).toBe(0);
  });
});

describe("scoreDecision", () => {
  const s = baseState();

  it("makes the total traceable to its weighted components", () => {
    const v = scoreDecision(s, 5000, 0, NOW);
    const weightSum = v.components.reduce((a, c) => a + c.weight, 0);
    expect(weightSum).toBeCloseTo(1, 5);
    const expected = Math.round(v.components.reduce((a, c) => a + c.score * c.weight, 0));
    expect(v.score).toBe(expected);
    // Every component carries the numbers behind it.
    expect(v.components.every((c) => c.detail.length > 10)).toBe(true);
  });

  it("scores a comfortably affordable purchase as BUY", () => {
    const v = scoreDecision(s, 5000, 0, NOW);
    expect(v.score).toBe(71);
    expect(v.label).toBe("ACCEPTABLE");
    expect(v.action).toBe("BUY");
    expect(v.assumptions.length).toBeGreaterThan(0);
  });

  it("rejects a purchase the user cannot pay for", () => {
    const v = scoreDecision(s, 200000, 0, NOW);
    expect(v.score).toBeLessThan(42);
    expect(v.label).toBe("IMPRUDENT");
    expect(v.action).toBe("DONT");
    expect(v.components.find((c) => c.key === "affordability")!.score).toBe(0);
  });

  it("raises a cheaper alternative when it fits far better", () => {
    // Tight cash: only 2 months of buffer, so the full 6,000 hurts.
    const tight = buildFinancialState(
      { currency: "INR", monthly_income: 4000, savings: 0 },
      [
        { amount: 4000, type: "income", occurred_at: iso(30) },
        { amount: 2100, type: "expense", occurred_at: iso(10) },
      ],
      goals,
      NOW,
    );
    const v = scoreDecision(tight, 6000, 0, NOW);
    if (v.action === "SPLIT") {
      expect(v.betterOption?.id).toBe("cheaper");
    } else {
      expect(["DONT", "WAIT"]).toContain(v.action);
    }
  });

  it("always returns four scenarios", () => {
    const v = scoreDecision(s, 5000, 0, NOW);
    expect(v.scenarios.map((x) => x.id)).toEqual(["now", "wait", "cheaper", "skip"]);
  });
});

describe("detectAlerts", () => {
  it("flags cash held above the planned buffer", () => {
    const alerts = detectAlerts(baseState(), txs, NOW);
    const cash = alerts.find((a) => a.key === "excess-cash");
    expect(cash).toBeDefined();
    expect(cash!.body).toContain("9,971");
  });

  it("flags a goal drifting past its deadline", () => {
    const drifting = buildFinancialState(
      profile,
      txs,
      [{ id: "g2", name: "Vacation", target_amount: 50000, current_amount: 14000, monthly_contribution: 500, target_date: new Date(NOW + 365 * 86_400_000).toISOString() }],
      NOW,
    );
    const alerts = detectAlerts(drifting, txs, NOW);
    const drift = alerts.find((a) => a.key.startsWith("goal-drift:"));
    expect(drift).toBeDefined();
    expect(drift!.severity).toBe("critical"); // ~5 years late
    expect(drift!.title).toContain("Vacation");
  });

  it("stays quiet when there is no data", () => {
    expect(detectAlerts(buildFinancialState(profile, [], [], NOW), [], NOW)).toEqual([]);
  });
});

describe("nextBestAction", () => {
  it("asks for data first on an empty ledger", () => {
    const a = nextBestAction(buildFinancialState(profile, [], goals, NOW), []);
    expect(a?.title).toBe("Add your first transactions");
  });

  it("prioritises a thin emergency reserve", () => {
    const lean = buildFinancialState(
      { currency: "INR", monthly_income: 4000, savings: 0 },
      [
        { amount: 4000, type: "income", occurred_at: iso(30) },
        { amount: 1000, type: "expense", occurred_at: iso(15) },
        { amount: 1100, type: "expense", occurred_at: iso(5) },
      ],
      goals,
      NOW,
    );
    expect(lean.reserveMonths).toBeLessThan(3);
    const a = nextBestAction(lean, []);
    expect(a?.title).toContain("emergency reserve");
    expect(a?.title).toContain("643");
  });

  it("recommends allocating excess cash when the buffer is healthy", () => {
    const a = nextBestAction(baseState(), detectAlerts(baseState(), txs, NOW));
    expect(a?.title).toContain("House down payment");
    expect(a?.why).toContain("buffer");
  });
});

describe("INR currency consistency", () => {
  // profile already carries currency: "INR" — the user's configured currency.
  const s = baseState();

  it("propagates the profile currency into the financial state", () => {
    expect(s.currency).toBe("INR");
    expect(buildFinancialState(null, [], [], NOW).currency).toBe("USD"); // fallback unchanged
  });

  it("formats every decision verdict figure with ₹, never a bare currency code", () => {
    const v = scoreDecision(s, 5000, 0, NOW);
    expect(v.components.every((c) => !/\bUSD\b|\bINR\b/.test(c.detail))).toBe(true);
    expect(v.components.find((c) => c.key === "affordability")!.detail).toContain("₹");
    expect(v.components.find((c) => c.key === "opportunity")!.detail).toContain("₹");
    expect(v.changesTheAnswer).toContain("₹");
  });

  it("formats alerts and the next best action in ₹", () => {
    const alerts = detectAlerts(s, txs, NOW);
    expect(alerts.length).toBeGreaterThan(0);
    expect(alerts.every((a) => !/\bUSD\b|\bINR\b/.test(a.body + (a.action ?? "")))).toBe(true);
    expect(alerts.find((a) => a.key === "excess-cash")!.body).toContain("₹");
    const action = nextBestAction(s, alerts);
    expect(action!.title).toContain("₹");
  });

  it("hands the INR currency to the AI context so coach replies match", () => {
    expect(intelligenceSummary(s).currency).toBe("INR");
  });
});
