from __future__ import annotations

import sys
from pathlib import Path
import unittest

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))

from uganda_payroll import (  # noqa: E402
    AmountMode,
    BaseEffect,
    EmploymentTaxMode,
    InputMode,
    PaymentCategory,
    PayrollBatchRequest,
    PayrollBatchResult,
    PayrollCalculator,
    PayrollLineItem,
    PayrollPeriod,
    PayrollRequest,
    PayslipSide,
    render_payslip_html,
)
from uganda_payroll.models import EmployeeProfile, EmployerProfile


class PayrollCalculatorTests(unittest.TestCase):
    def setUp(self) -> None:
        self.calculator = PayrollCalculator()
        self.period = PayrollPeriod.from_month(2026, 1)

    def _request(self, *, mode=EmploymentTaxMode.PRIMARY_RESIDENT, input_mode=InputMode.BASIC_PAY, amount=0, items=()):
        from uganda_payroll import EmployerProfile, EmployeeProfile

        return PayrollRequest(
            employer=EmployerProfile(name="Helpware Tech Uganda Limited", tin="1000943373"),
            employee=EmployeeProfile(
                full_name="Simon Mukisa",
                staff_number="4954",
                tin="1019532691",
                nssf_number="9317200292098",
                department="Operations",
                position="Advisor",
                employment_tax_mode=mode,
            ),
            period=self.period,
            input_mode=input_mode,
            target_amount=amount,
            items=items,
        )

    def test_workbook_regression_for_january_2026_row(self) -> None:
        overtime = PaymentCategory.from_nature(
            code="overtime",
            name="Over Time",
            nature="Increment",
            attracts_paye=True,
            attracts_nssf=True,
        )

        result = self.calculator.calculate(
            self._request(
                amount=3_870_000,
                items=(PayrollLineItem(category=overtime, amount=333_000),),
            )
        )

        self.assertEqual(result.gross_earnings, 4_203_000)
        self.assertEqual(result.chargeable_income, 4_203_000)
        self.assertEqual(result.nssf_employee, 210_150)
        self.assertEqual(result.total_paye, 1_162_900)
        self.assertEqual(result.total_deductions, 1_373_050)
        self.assertEqual(result.net_pay, 2_829_950)
        self.assertEqual(result.nssf_employer, 420_300)

    def test_secondary_employment_uses_flat_thirty_percent(self) -> None:
        result = self.calculator.calculate(
            self._request(
                mode=EmploymentTaxMode.SECONDARY,
                amount=1_000_000,
            )
        )

        self.assertEqual(result.total_paye, 300_000)
        self.assertEqual(result.nssf_employee, 50_000)
        self.assertEqual(result.net_pay, 650_000)

    def test_net_target_solves_for_basic_pay(self) -> None:
        transport = PaymentCategory.from_nature(
            code="transport",
            name="Transport",
            nature="Increment",
            attracts_paye=False,
            attracts_nssf=False,
        )
        sacco = PaymentCategory.from_nature(
            code="sacco",
            name="SACCO",
            nature="Deduction",
            attracts_paye=False,
            attracts_nssf=False,
        )
        result = self.calculator.calculate(
            self._request(
                input_mode=InputMode.NET_PAY,
                amount=1_500_000,
                items=(
                    PayrollLineItem(category=transport, amount=50_000),
                    PayrollLineItem(category=sacco, amount=10_000),
                ),
            )
        )

        self.assertEqual(result.net_pay, 1_500_000)
        self.assertGreater(result.basic_pay, 0)

    def test_taxable_memo_item_affects_tax_not_cash_gross(self) -> None:
        benefit = PaymentCategory.from_nature(
            code="housing_benefit",
            name="Housing Benefit",
            nature="Do nothing",
            attracts_paye=True,
            attracts_nssf=False,
            payslip_side=PayslipSide.MEMO,
            cash_effect=BaseEffect.IGNORE,
        )
        without_benefit = self.calculator.calculate(self._request(amount=500_000))
        with_benefit = self.calculator.calculate(
            self._request(
                amount=500_000,
                items=(PayrollLineItem(category=benefit, amount=100_000),),
            )
        )

        self.assertEqual(with_benefit.gross_earnings, without_benefit.gross_earnings)
        self.assertGreater(with_benefit.total_paye, without_benefit.total_paye)
        self.assertLess(with_benefit.net_pay, without_benefit.net_pay)

    def test_payslip_html_contains_expected_sections(self) -> None:
        bonus = PaymentCategory.from_nature(
            code="bonus",
            name="Bonus",
            nature="Bonus",
            attracts_paye=True,
            attracts_nssf=True,
        )
        result = self.calculator.calculate(
            self._request(
                amount=1_000_000,
                items=(PayrollLineItem(category=bonus, amount=100_000),),
            )
        )
        html = render_payslip_html(result)

        self.assertIn("PAYMENT SLIP", html)
        self.assertIn("Simon Mukisa", html)
        self.assertIn("Bonus", html)
        self.assertIn("Net Pay", html)


class FromHrmTests(unittest.TestCase):
    def test_taxable_nssfable_increment(self) -> None:
        cat = PaymentCategory.from_hrm(code="ot", name="Overtime", nature="Increment", taxable=True, nssfable=True)
        self.assertEqual(cat.paye_base_effect, BaseEffect.ADD)
        self.assertEqual(cat.nssf_employee_base_effect, BaseEffect.ADD)
        self.assertEqual(cat.cash_effect, BaseEffect.ADD)

    def test_non_taxable_increment(self) -> None:
        cat = PaymentCategory.from_hrm(code="tr", name="Transport", nature="Increment", taxable=False, nssfable=False)
        self.assertEqual(cat.paye_base_effect, BaseEffect.IGNORE)
        self.assertEqual(cat.nssf_employee_base_effect, BaseEffect.IGNORE)

    def test_deduction(self) -> None:
        cat = PaymentCategory.from_hrm(code="sac", name="SACCO", nature="Deduction")
        self.assertEqual(cat.cash_effect, BaseEffect.SUBTRACT)
        self.assertEqual(cat.paye_base_effect, BaseEffect.IGNORE)

    def test_do_nothing(self) -> None:
        cat = PaymentCategory.from_hrm(code="hb", name="Housing", nature="Do nothing", taxable=True)
        self.assertEqual(cat.cash_effect, BaseEffect.IGNORE)
        self.assertEqual(cat.paye_base_effect, BaseEffect.ADD)


class PresetCategoryTests(unittest.TestCase):
    def test_overtime(self) -> None:
        cat = PaymentCategory.overtime()
        self.assertEqual(cat.paye_base_effect, BaseEffect.ADD)
        self.assertEqual(cat.nssf_employee_base_effect, BaseEffect.ADD)
        self.assertEqual(cat.cash_effect, BaseEffect.ADD)

    def test_transport(self) -> None:
        cat = PaymentCategory.transport()
        self.assertEqual(cat.paye_base_effect, BaseEffect.IGNORE)
        self.assertEqual(cat.nssf_employee_base_effect, BaseEffect.IGNORE)

    def test_housing_benefit(self) -> None:
        cat = PaymentCategory.housing_benefit()
        self.assertEqual(cat.cash_effect, BaseEffect.IGNORE)
        self.assertEqual(cat.paye_base_effect, BaseEffect.ADD)
        self.assertEqual(cat.payslip_side, PayslipSide.MEMO)

    def test_sacco_deduction(self) -> None:
        cat = PaymentCategory.sacco_deduction()
        self.assertEqual(cat.cash_effect, BaseEffect.SUBTRACT)

    def test_bonus(self) -> None:
        cat = PaymentCategory.bonus()
        self.assertEqual(cat.paye_base_effect, BaseEffect.ADD)
        self.assertEqual(cat.nssf_employee_base_effect, BaseEffect.ADD)

    def test_payroll_correction(self) -> None:
        cat = PaymentCategory.payroll_correction()
        self.assertEqual(cat.paye_base_effect, BaseEffect.SUBTRACT)
        self.assertEqual(cat.nssf_employee_base_effect, BaseEffect.IGNORE)
        self.assertEqual(cat.cash_effect, BaseEffect.IGNORE)

    def test_medical_insurance(self) -> None:
        cat = PaymentCategory.medical_insurance()
        self.assertEqual(cat.cash_effect, BaseEffect.IGNORE)
        self.assertEqual(cat.employer_cost_effect, BaseEffect.ADD)
        self.assertEqual(cat.payslip_side, PayslipSide.EMPLOYER_CONTRIBUTIONS)

    def test_gratuity(self) -> None:
        cat = PaymentCategory.gratuity()
        self.assertEqual(cat.cash_effect, BaseEffect.ADD)
        self.assertEqual(cat.paye_base_effect, BaseEffect.IGNORE)

    def test_pension_deduction(self) -> None:
        cat = PaymentCategory.pension_deduction()
        self.assertEqual(cat.cash_effect, BaseEffect.SUBTRACT)
        self.assertEqual(cat.paye_base_effect, BaseEffect.SUBTRACT)

    def test_notice_pay(self) -> None:
        cat = PaymentCategory.notice_pay()
        self.assertEqual(cat.paye_base_effect, BaseEffect.ADD)
        self.assertEqual(cat.nssf_employee_base_effect, BaseEffect.ADD)

    def test_leave_compensation(self) -> None:
        cat = PaymentCategory.leave_compensation()
        self.assertEqual(cat.paye_base_effect, BaseEffect.ADD)
        self.assertEqual(cat.nssf_employee_base_effect, BaseEffect.ADD)

    def test_custom_code_and_name(self) -> None:
        cat = PaymentCategory.overtime(code="ot_weekend", name="Weekend OT")
        self.assertEqual(cat.code, "ot_weekend")
        self.assertEqual(cat.name, "Weekend OT")


class PercentageAmountTests(unittest.TestCase):
    def setUp(self) -> None:
        self.calculator = PayrollCalculator()
        self.period = PayrollPeriod.from_month(2026, 1)

    def _request(self, amount=0, items=()):
        return PayrollRequest(
            employer=EmployerProfile(name="Test Co"),
            employee=EmployeeProfile(full_name="Test Employee"),
            period=self.period,
            input_mode=InputMode.BASIC_PAY,
            target_amount=amount,
            items=items,
        )

    def test_percentage_of_basic(self) -> None:
        cat = PaymentCategory.overtime()
        item = PayrollLineItem(category=cat, amount=10, amount_mode=AmountMode.PERCENTAGE_OF_BASIC)
        result = self.calculator.calculate(self._request(amount=1_000_000, items=(item,)))
        # 10% of 1,000,000 = 100,000
        self.assertEqual(result.gross_earnings, 1_100_000)

    def test_percentage_of_gross(self) -> None:
        ot_cat = PaymentCategory.overtime()
        fixed_item = PayrollLineItem(category=ot_cat, amount=200_000)
        pct_cat = PaymentCategory.bonus()
        pct_item = PayrollLineItem(category=pct_cat, amount=10, amount_mode=AmountMode.PERCENTAGE_OF_GROSS)
        result = self.calculator.calculate(
            self._request(amount=1_000_000, items=(fixed_item, pct_item))
        )
        # Gross = 1,000,000 + 200,000 = 1,200,000; 10% of 1,200,000 = 120,000
        self.assertEqual(result.gross_earnings, 1_320_000)


class PayrollCorrectionTests(unittest.TestCase):
    def setUp(self) -> None:
        self.calculator = PayrollCalculator()
        self.period = PayrollPeriod.from_month(2026, 1)

    def _request(self, amount=0, items=()):
        return PayrollRequest(
            employer=EmployerProfile(name="Test Co"),
            employee=EmployeeProfile(full_name="Test Employee"),
            period=self.period,
            input_mode=InputMode.BASIC_PAY,
            target_amount=amount,
            items=items,
        )

    def test_payroll_correction_reduces_paye_not_nssf(self) -> None:
        base_result = self.calculator.calculate(self._request(amount=3_000_000))
        correction = PaymentCategory.payroll_correction()
        corrected_result = self.calculator.calculate(
            self._request(amount=3_000_000, items=(PayrollLineItem(category=correction, amount=500_000),))
        )
        # NSSF should remain unchanged (correction doesn't affect NSSF base)
        self.assertEqual(corrected_result.nssf_employee, base_result.nssf_employee)
        self.assertEqual(corrected_result.nssf_employer, base_result.nssf_employer)
        # PAYE should decrease (correction reduces chargeable income)
        self.assertLess(corrected_result.total_paye, base_result.total_paye)
        # Net pay should increase (lower PAYE = more take-home)
        self.assertGreater(corrected_result.net_pay, base_result.net_pay)


class BatchCalculationTests(unittest.TestCase):
    def setUp(self) -> None:
        self.calculator = PayrollCalculator()
        self.period = PayrollPeriod.from_month(2026, 1)
        self.employer = EmployerProfile(name="Helpware Tech", tin="1000943373")

    def _make_request(self, name, amount, items=()):
        return PayrollRequest(
            employer=self.employer,
            employee=EmployeeProfile(full_name=name),
            period=self.period,
            input_mode=InputMode.BASIC_PAY,
            target_amount=amount,
            items=items,
        )

    def test_batch_totals_match_individual_sums(self) -> None:
        req1 = self._make_request("Employee A", 2_000_000)
        req2 = self._make_request("Employee B", 3_000_000)
        batch = PayrollBatchRequest(
            employer=self.employer,
            period=self.period,
            employee_requests=(req1, req2),
        )
        batch_result = self.calculator.calculate_batch(batch)
        r1 = self.calculator.calculate(req1)
        r2 = self.calculator.calculate(req2)

        self.assertEqual(len(batch_result.results), 2)
        self.assertEqual(batch_result.total_basic_pay, r1.basic_pay + r2.basic_pay)
        self.assertEqual(batch_result.total_gross_earnings, r1.gross_earnings + r2.gross_earnings)
        self.assertEqual(batch_result.total_nssf_employee, r1.nssf_employee + r2.nssf_employee)
        self.assertEqual(batch_result.total_nssf_employer, r1.nssf_employer + r2.nssf_employer)
        self.assertEqual(batch_result.total_paye, r1.total_paye + r2.total_paye)
        self.assertEqual(batch_result.total_net_pay, r1.net_pay + r2.net_pay)
        self.assertEqual(batch_result.total_employer_cost, r1.total_employer_cost + r2.total_employer_cost)


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