# FILE: tests/unit/test_calculators.py
# Unit tests for tax calculation logic

import os
import sys
from decimal import Decimal
from typing import Any

import pytest

# Add libs to path for testing
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "libs"))

# Mock the calculation functions since they're in the service
# In a real implementation, these would be extracted to shared libs

# pylint: disable=wrong-import-position,import-error,too-few-public-methods,global-statement
# pylint: disable=raise-missing-from,unused-argument,too-many-arguments,too-many-positional-arguments
# pylint: disable=too-many-locals,import-outside-toplevel
# mypy: disable-error-code=union-attr
# mypy: disable-error-code=no-untyped-def


class MockTaxCalculator:
    """Mock tax calculator for testing"""

    def __init__(self, tax_year: str = "2023-24"):
        self.tax_year = tax_year
        self.precision = 2

    def compute_sa103_self_employment(
        self, income_items: list[dict[str, Any]], expense_items: list[dict[str, Any]]
    ) -> dict[str, Any]:
        """Compute SA103 self-employment schedule"""

        total_turnover = Decimal("0")
        total_expenses = Decimal("0")
        evidence_trail = []

        # Sum income
        for income in income_items:
            if income.get("type") == "self_employment":
                amount = Decimal(str(income.get("gross", 0)))
                total_turnover += amount

                evidence_trail.append(
                    {
                        "box": "20",
                        "source_entity": income.get("income_id"),
                        "amount": float(amount),
                        "description": f"Income: {income.get('description', 'Unknown')}",
                    }
                )

        # Sum expenses
        for expense in expense_items:
            if expense.get("allowable", True):
                amount = Decimal(str(expense.get("amount", 0)))
                total_expenses += amount

                evidence_trail.append(
                    {
                        "box": "31",
                        "source_entity": expense.get("expense_id"),
                        "amount": float(amount),
                        "description": f"Expense: {expense.get('description', 'Unknown')}",
                    }
                )

        # Calculate net profit
        net_profit = total_turnover - total_expenses

        # Create form boxes
        form_boxes = {
            "20": {
                "value": float(total_turnover),
                "description": "Total turnover",
                "confidence": 0.9,
            },
            "31": {
                "value": float(total_expenses),
                "description": "Total allowable business expenses",
                "confidence": 0.9,
            },
            "32": {
                "value": float(net_profit),
                "description": "Net profit",
                "confidence": 0.9,
            },
        }

        return {
            "form_boxes": form_boxes,
            "evidence_trail": evidence_trail,
            "total_turnover": float(total_turnover),
            "total_expenses": float(total_expenses),
            "net_profit": float(net_profit),
        }

    def compute_sa105_property(
        self, income_items: list[dict[str, Any]], expense_items: list[dict[str, Any]]
    ) -> dict[str, Any]:
        """Compute SA105 property income schedule"""

        total_rents = Decimal("0")
        total_property_expenses = Decimal("0")
        evidence_trail = []

        # Sum property income
        for income in income_items:
            if income.get("type") == "property":
                amount = Decimal(str(income.get("gross", 0)))
                total_rents += amount

                evidence_trail.append(
                    {
                        "box": "20",
                        "source_entity": income.get("income_id"),
                        "amount": float(amount),
                        "description": f"Property income: {income.get('description', 'Unknown')}",
                    }
                )

        # Sum property expenses
        for expense in expense_items:
            if expense.get("type") == "property" and expense.get("allowable", True):
                amount = Decimal(str(expense.get("amount", 0)))
                total_property_expenses += amount

                # Map to appropriate SA105 box based on expense category
                box = self._map_property_expense_to_box(
                    expense.get("category", "other")
                )

                evidence_trail.append(
                    {
                        "box": box,
                        "source_entity": expense.get("expense_id"),
                        "amount": float(amount),
                        "description": f"Property expense: {expense.get('description', 'Unknown')}",
                    }
                )

        # Calculate net property income
        net_property_income = total_rents - total_property_expenses

        form_boxes = {
            "20": {
                "value": float(total_rents),
                "description": "Total rents and other income",
                "confidence": 0.9,
            },
            "38": {
                "value": float(total_property_expenses),
                "description": "Total property expenses",
                "confidence": 0.9,
            },
            "net_income": {
                "value": float(net_property_income),
                "description": "Net property income",
                "confidence": 0.9,
            },
        }

        return {
            "form_boxes": form_boxes,
            "evidence_trail": evidence_trail,
            "total_rents": float(total_rents),
            "total_expenses": float(total_property_expenses),
            "net_income": float(net_property_income),
        }

    def _map_property_expense_to_box(self, category: str) -> str:
        """Map property expense category to SA105 box"""
        mapping = {
            "rent_rates_insurance": "31",
            "property_management": "32",
            "services_wages": "33",
            "repairs_maintenance": "34",
            "finance_costs": "35",
            "professional_fees": "36",
            "costs_of_services": "37",
            "other": "38",
        }

        return mapping.get(category, "38")


class TestSA103SelfEmployment:
    """Test SA103 self-employment calculations"""

    @pytest.fixture
    def calculator(self):
        return MockTaxCalculator("2023-24")

    @pytest.fixture
    def sample_income_items(self):
        return [
            {
                "income_id": "income_1",
                "type": "self_employment",
                "gross": 75000,
                "description": "Consulting income",
            },
            {
                "income_id": "income_2",
                "type": "self_employment",
                "gross": 25000,
                "description": "Training income",
            },
        ]

    @pytest.fixture
    def sample_expense_items(self):
        return [
            {
                "expense_id": "expense_1",
                "type": "self_employment",
                "amount": 5000,
                "allowable": True,
                "description": "Office rent",
            },
            {
                "expense_id": "expense_2",
                "type": "self_employment",
                "amount": 2000,
                "allowable": True,
                "description": "Equipment",
            },
            {
                "expense_id": "expense_3",
                "type": "self_employment",
                "amount": 1000,
                "allowable": False,
                "description": "Entertainment (not allowable)",
            },
        ]

    def test_basic_calculation(
        self, calculator, sample_income_items, sample_expense_items
    ):
        """Test basic SA103 calculation"""

        result = calculator.compute_sa103_self_employment(
            sample_income_items, sample_expense_items
        )

        # Check totals
        assert result["total_turnover"] == 100000  # 75000 + 25000
        assert result["total_expenses"] == 7000  # 5000 + 2000 (excluding non-allowable)
        assert result["net_profit"] == 93000  # 100000 - 7000

        # Check form boxes
        form_boxes = result["form_boxes"]
        assert form_boxes["20"]["value"] == 100000
        assert form_boxes["31"]["value"] == 7000
        assert form_boxes["32"]["value"] == 93000

        # Check evidence trail
        evidence_trail = result["evidence_trail"]
        assert len(evidence_trail) == 4  # 2 income + 2 allowable expenses

    def test_zero_income(self, calculator):
        """Test calculation with zero income"""

        result = calculator.compute_sa103_self_employment([], [])

        assert result["total_turnover"] == 0
        assert result["total_expenses"] == 0
        assert result["net_profit"] == 0

        form_boxes = result["form_boxes"]
        assert form_boxes["20"]["value"] == 0
        assert form_boxes["31"]["value"] == 0
        assert form_boxes["32"]["value"] == 0

    def test_loss_scenario(self, calculator):
        """Test calculation resulting in a loss"""

        income_items = [
            {
                "income_id": "income_1",
                "type": "self_employment",
                "gross": 10000,
                "description": "Low income year",
            }
        ]

        expense_items = [
            {
                "expense_id": "expense_1",
                "type": "self_employment",
                "amount": 15000,
                "allowable": True,
                "description": "High expenses",
            }
        ]

        result = calculator.compute_sa103_self_employment(income_items, expense_items)

        assert result["total_turnover"] == 10000
        assert result["total_expenses"] == 15000
        assert result["net_profit"] == -5000  # Loss

        form_boxes = result["form_boxes"]
        assert form_boxes["32"]["value"] == -5000

    def test_non_allowable_expenses_excluded(self, calculator, sample_income_items):
        """Test that non-allowable expenses are excluded"""

        expense_items = [
            {
                "expense_id": "expense_1",
                "type": "self_employment",
                "amount": 5000,
                "allowable": True,
                "description": "Allowable expense",
            },
            {
                "expense_id": "expense_2",
                "type": "self_employment",
                "amount": 3000,
                "allowable": False,
                "description": "Non-allowable expense",
            },
        ]

        result = calculator.compute_sa103_self_employment(
            sample_income_items, expense_items
        )

        # Only allowable expenses should be included
        assert result["total_expenses"] == 5000

        # Evidence trail should only include allowable expenses
        expense_evidence = [e for e in result["evidence_trail"] if e["box"] == "31"]
        assert len(expense_evidence) == 1
        assert expense_evidence[0]["amount"] == 5000


class TestSA105Property:
    """Test SA105 property income calculations"""

    @pytest.fixture
    def calculator(self):
        return MockTaxCalculator("2023-24")

    @pytest.fixture
    def sample_property_income(self):
        return [
            {
                "income_id": "prop_income_1",
                "type": "property",
                "gross": 24000,
                "description": "Rental income - Property 1",
            },
            {
                "income_id": "prop_income_2",
                "type": "property",
                "gross": 18000,
                "description": "Rental income - Property 2",
            },
        ]

    @pytest.fixture
    def sample_property_expenses(self):
        return [
            {
                "expense_id": "prop_expense_1",
                "type": "property",
                "amount": 3000,
                "allowable": True,
                "category": "rent_rates_insurance",
                "description": "Insurance and rates",
            },
            {
                "expense_id": "prop_expense_2",
                "type": "property",
                "amount": 2000,
                "allowable": True,
                "category": "repairs_maintenance",
                "description": "Repairs and maintenance",
            },
            {
                "expense_id": "prop_expense_3",
                "type": "property",
                "amount": 1500,
                "allowable": True,
                "category": "property_management",
                "description": "Property management fees",
            },
        ]

    def test_basic_property_calculation(
        self, calculator, sample_property_income, sample_property_expenses
    ):
        """Test basic SA105 property calculation"""

        result = calculator.compute_sa105_property(
            sample_property_income, sample_property_expenses
        )

        # Check totals
        assert result["total_rents"] == 42000  # 24000 + 18000
        assert result["total_expenses"] == 6500  # 3000 + 2000 + 1500
        assert result["net_income"] == 35500  # 42000 - 6500

        # Check form boxes
        form_boxes = result["form_boxes"]
        assert form_boxes["20"]["value"] == 42000
        assert form_boxes["38"]["value"] == 6500
        assert form_boxes["net_income"]["value"] == 35500

    def test_property_expense_mapping(self, calculator):
        """Test property expense category mapping to form boxes"""

        # Test different expense categories
        test_cases = [
            ("rent_rates_insurance", "31"),
            ("property_management", "32"),
            ("services_wages", "33"),
            ("repairs_maintenance", "34"),
            ("finance_costs", "35"),
            ("professional_fees", "36"),
            ("costs_of_services", "37"),
            ("other", "38"),
            ("unknown_category", "38"),  # Should default to 38
        ]

        for category, expected_box in test_cases:
            actual_box = calculator._map_property_expense_to_box(category)
            assert (
                actual_box == expected_box
            ), f"Category {category} should map to box {expected_box}"

    def test_property_loss(self, calculator):
        """Test property calculation resulting in a loss"""

        income_items = [
            {
                "income_id": "prop_income_1",
                "type": "property",
                "gross": 12000,
                "description": "Low rental income",
            }
        ]

        expense_items = [
            {
                "expense_id": "prop_expense_1",
                "type": "property",
                "amount": 15000,
                "allowable": True,
                "category": "repairs_maintenance",
                "description": "Major repairs",
            }
        ]

        result = calculator.compute_sa105_property(income_items, expense_items)

        assert result["total_rents"] == 12000
        assert result["total_expenses"] == 15000
        assert result["net_income"] == -3000  # Loss

        form_boxes = result["form_boxes"]
        assert form_boxes["net_income"]["value"] == -3000


class TestCalculationEdgeCases:
    """Test edge cases and error conditions"""

    @pytest.fixture
    def calculator(self):
        return MockTaxCalculator("2023-24")

    def test_decimal_precision(self, calculator):
        """Test decimal precision handling"""

        income_items = [
            {
                "income_id": "income_1",
                "type": "self_employment",
                "gross": 33333.33,
                "description": "Precise income",
            }
        ]

        expense_items = [
            {
                "expense_id": "expense_1",
                "type": "self_employment",
                "amount": 11111.11,
                "allowable": True,
                "description": "Precise expense",
            }
        ]

        result = calculator.compute_sa103_self_employment(income_items, expense_items)

        # Check that calculations maintain precision
        assert result["total_turnover"] == 33333.33
        assert result["total_expenses"] == 11111.11
        assert result["net_profit"] == 22222.22

    def test_string_amounts(self, calculator):
        """Test handling of string amounts"""

        income_items = [
            {
                "income_id": "income_1",
                "type": "self_employment",
                "gross": "50000.00",  # String amount
                "description": "String income",
            }
        ]

        expense_items = [
            {
                "expense_id": "expense_1",
                "type": "self_employment",
                "amount": "10000.00",  # String amount
                "allowable": True,
                "description": "String expense",
            }
        ]

        result = calculator.compute_sa103_self_employment(income_items, expense_items)

        assert result["total_turnover"] == 50000.0
        assert result["total_expenses"] == 10000.0
        assert result["net_profit"] == 40000.0

    def test_missing_fields(self, calculator):
        """Test handling of missing fields"""

        income_items = [
            {
                "income_id": "income_1",
                "type": "self_employment",
                # Missing 'gross' field
                "description": "Income without amount",
            }
        ]

        expense_items = [
            {
                "expense_id": "expense_1",
                "type": "self_employment",
                # Missing 'amount' field
                "allowable": True,
                "description": "Expense without amount",
            }
        ]

        result = calculator.compute_sa103_self_employment(income_items, expense_items)

        # Should handle missing fields gracefully
        assert result["total_turnover"] == 0
        assert result["total_expenses"] == 0
        assert result["net_profit"] == 0


if __name__ == "__main__":
    # Run the tests
    pytest.main([__file__, "-v"])
