"""
Unit tests for server/forensic_engines.py.

Run with: cd server && ../venv/bin/python3 -m unittest tests.test_forensic_engines -v
"""
import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import forensic_engines as fe  # noqa: E402


class BenfordTests(unittest.TestCase):
    def test_too_few_values_returns_none(self):
        out = fe.benford_analysis([100, 200, 300])
        self.assertIsNone(out["conformity"])

    def test_conforming_population(self):
        # A geometric-ish spread across leading digits, weighted like a real
        # Benford population (lots of 1s and 2s, few 8s and 9s).
        amounts = ([111, 123, 156, 187, 145, 134, 198, 176, 189, 112] * 3 +
                   [211, 234, 256, 278, 289] * 2 + [311, 345, 356] * 2 +
                   [411, 456] + [511] + [611] + [711] + [811] + [911])
        out = fe.benford_analysis(amounts)
        self.assertIsNotNone(out["conformity"])
        self.assertGreaterEqual(out["count"], 30)

    def test_ignores_zero_and_none(self):
        out = fe.benford_analysis([0, None] + [100] * 40)
        self.assertEqual(out["count"], 40)


class DuplicateTests(unittest.TestCase):
    def test_finds_duplicate_cluster(self):
        rows = [{"party": "A", "amount": 100}, {"party": "A", "amount": 100}, {"party": "B", "amount": 200}]
        out = fe.detect_duplicates(rows, ["party", "amount"])
        self.assertEqual(len(out), 1)
        self.assertEqual(out[0]["count"], 2)

    def test_case_insensitive_matching(self):
        rows = [{"party": "Vendor A"}, {"party": "vendor a"}]
        out = fe.detect_duplicates(rows, ["party"])
        self.assertEqual(len(out), 1)

    def test_blank_key_excluded(self):
        rows = [{"party": "", "amount": 100}, {"party": "", "amount": 100}]
        out = fe.detect_duplicates(rows, ["party", "amount"])
        self.assertEqual(out, [])

    def test_no_false_positive_on_distinct_rows(self):
        rows = [{"party": "A", "amount": 100}, {"party": "B", "amount": 200}]
        self.assertEqual(fe.detect_duplicates(rows, ["party", "amount"]), [])


class RoundNumberTests(unittest.TestCase):
    def test_flags_round_amount_above_threshold(self):
        rows = [{"amount": 50000}, {"amount": 12345}, {"amount": 9000}]
        out = fe.detect_round_numbers(rows, round_to=1000, min_amount=10000)
        self.assertEqual(out, [0])

    def test_below_min_amount_not_flagged(self):
        rows = [{"amount": 1000}]
        self.assertEqual(fe.detect_round_numbers(rows, round_to=1000, min_amount=10000), [])


class RelatedPartyTests(unittest.TestCase):
    def test_flags_matching_party(self):
        rows = [{"party": "Related Co"}, {"party": "Unrelated Co"}]
        out = fe.detect_related_party(rows, ["Related Co"])
        self.assertEqual(out, [0])

    def test_empty_list_flags_nothing(self):
        rows = [{"party": "Related Co"}]
        self.assertEqual(fe.detect_related_party(rows, []), [])


class JournalEntryScoreTests(unittest.TestCase):
    def test_clean_entry_is_low(self):
        out = fe.score_journal_entry({"date": "2026-08-25", "amount": 1234, "description": "Rent payment", "reference_no": "JV-01"})
        self.assertEqual(out["risk_level"], "Low")

    def test_multiple_flags_escalate(self):
        out = fe.score_journal_entry({"date": "2026-08-23", "amount": 50000, "description": "", "reference_no": ""})
        self.assertIn(out["risk_level"], ("Medium", "High"))
        self.assertGreaterEqual(out["score"], 4)


class PayrollTests(unittest.TestCase):
    def test_duplicate_bank_account_flagged(self):
        rows = [{"extra": {"bank_account": "111", "pan_no": "A1", "address": "X"}},
                {"extra": {"bank_account": "111", "pan_no": "A2", "address": "Y"}}]
        findings = fe.analyze_payroll(rows)
        types = {f["type"] for f in findings}
        self.assertIn("duplicate_bank_account", types)

    def test_missing_data_flagged(self):
        rows = [{"extra": {}}]
        findings = fe.analyze_payroll(rows)
        self.assertTrue(any(f["type"] == "missing_employee_data" for f in findings))


class VendorTests(unittest.TestCase):
    def test_vendor_matches_employee_bank(self):
        vendors = [{"extra": {"bank_account": "999"}}]
        employees = [{"extra": {"bank_account": "999"}}]
        findings = fe.analyze_vendors(vendors, employees)
        self.assertTrue(any(f["type"] == "vendor_matches_employee_bank" for f in findings))

    def test_no_employee_rows_no_crash(self):
        self.assertEqual(fe.analyze_vendors([{"extra": {}}], None), [])


class ExpenseTests(unittest.TestCase):
    def test_just_under_threshold(self):
        rows = [{"amount": 4900, "date": "2026-08-24"}]
        findings = fe.analyze_expenses(rows, approval_threshold=5000)
        self.assertTrue(any(f["type"] == "just_under_threshold" for f in findings))

    def test_duplicate_claim(self):
        rows = [{"party": "Emp A", "amount": 2500, "date": "2026-08-20"},
                {"party": "Emp A", "amount": 2500, "date": "2026-08-20"}]
        findings = fe.analyze_expenses(rows)
        self.assertTrue(any(f["type"] == "duplicate_expense_claim" for f in findings))


class BeneishTests(unittest.TestCase):
    def _base(self):
        return {"receivables": 500000, "sales": 2000000, "cogs": 1200000, "current_assets": 800000,
                "ppe": 1500000, "total_assets": 3000000, "depreciation": 150000, "sga": 300000,
                "current_liabilities": 400000, "total_debt": 600000, "net_income": 250000, "cfo": 180000}

    def test_missing_input_returns_none_score(self):
        out = fe.beneish_m_score({"sales": 100}, {})
        self.assertIsNone(out["m_score"])
        self.assertIsNotNone(out["note"])

    def test_computes_with_full_inputs(self):
        curr = self._base()
        prior = dict(curr, sales=1800000, receivables=400000)
        out = fe.beneish_m_score(curr, prior)
        self.assertIsNotNone(out["m_score"])
        self.assertIn("interpretation", out)
        self.assertEqual(out["threshold"], -1.78)

    def test_aggressive_receivables_growth_raises_score(self):
        prior = self._base()
        curr_normal = dict(prior, sales=2000000)
        curr_aggressive = dict(prior, sales=2000000, receivables=1200000)
        m_normal = fe.beneish_m_score(curr_normal, prior)["m_score"]
        m_aggressive = fe.beneish_m_score(curr_aggressive, prior)["m_score"]
        self.assertGreater(m_aggressive, m_normal)


class DispatcherTests(unittest.TestCase):
    def test_journal_entries_runs_applicable_detectors(self):
        rows = [{"date": "2026-08-22", "amount": 50000, "description": "", "reference_no": "", "party": ""}] * 5
        out = fe.run_all_detectors("journal_entries", rows)
        detectors = {f["detector"] for f in out}
        self.assertTrue(detectors.issubset({"benford", "round_number", "unusual_je", "related_party"}))

    def test_unknown_dataset_type_returns_empty(self):
        self.assertEqual(fe.run_all_detectors("unknown_type", [{"amount": 100}]), [])

    def test_payroll_dataset_runs_payroll_detectors_only(self):
        rows = [{"extra": {"bank_account": "1", "pan_no": "A", "address": "X"}},
                {"extra": {"bank_account": "1", "pan_no": "B", "address": "Y"}}]
        out = fe.run_all_detectors("payroll", rows)
        detectors = {f["detector"] for f in out}
        self.assertTrue(detectors.issubset({"payroll_fraud", "ghost_employee"}))
        self.assertTrue(any(f["detector"] == "ghost_employee" for f in out))


if __name__ == "__main__":
    unittest.main()
