"""Analytical, grid-boundary and budget tests for the assumed examples."""

import json
import unittest
from decimal import Decimal
from pathlib import Path
from uuid import uuid4

import capital_calculator as calculator

D = Decimal


class CapitalTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.protocol = json.loads((Path(__file__).resolve().parent / "protocol.json").read_text(encoding="utf-8"), parse_float=D)
        cls.sizing = cls.protocol["position_sizing"]

    def test_known_quantities_and_planned_losses(self):
        quantities = [D("0.43"), D("2.17"), D("8.69"), D("43.47")]
        losses = [D("0.4945"), D("2.4955"), D("9.9935"), D("49.9905")]
        for equity, quantity, loss in zip(self.protocol["equities"], quantities, losses):
            result = calculator.size_position(equity, self.sizing)
            self.assertEqual(result["quantity"], quantity)
            self.assertEqual(result["conservative_planned_gap_free_loss"], loss)
            self.assertLessEqual(loss, result["risk_budget"])
            self.assertLessEqual(result["entry_cash_required"], equity)

    def test_minimum_boundary_skips_instead_of_rounding_up(self):
        self.assertEqual(calculator.minimum_executable_capital(self.sizing)["minimum_equity_satisfying_both"], D("46"))
        below = calculator.size_position(D("45.99"), self.sizing)
        boundary = calculator.size_position(D("46"), self.sizing)
        self.assertEqual(below["status"], "SKIP_BELOW_ASSUMED_MINIMUM")
        self.assertEqual(below["quantity"], 0)
        self.assertEqual(boundary["quantity"], D("0.20"))
        self.assertEqual(boundary["conservative_planned_gap_free_loss"], D("0.23000"))
        self.assertEqual(boundary["assumed_stopped_exit_notional_before_exit_fee"], D("9.79510000"))
        self.assertFalse(boundary["meets_assumed_same_minimum_on_stopped_exit"])
        diagnostic = calculator.minimum_executable_capital(self.sizing)["assumed_full_quantity_stop_exit_minimum_diagnostic"]
        self.assertEqual(diagnostic["minimum_entry_quantity_also_meeting_stated_stop_exit_minimum"], D("0.21"))
        self.assertEqual(diagnostic["minimum_equity_for_entry_and_assumed_stop_exit_minimum"], D("48.3"))
        self.assertFalse(calculator.size_position(D("48.299"), self.sizing)["meets_assumed_same_minimum_on_stopped_exit"])
        self.assertTrue(calculator.size_position(D("48.3"), self.sizing)["meets_assumed_same_minimum_on_stopped_exit"])

    def test_coarse_quantity_grid_raises_feasibility_boundary(self):
        settings = {**self.sizing, **self.sizing["coarse_grid_example"]}
        result = calculator.minimum_executable_capital(settings)
        self.assertEqual(result["minimum_executable_quantity"], D("0.21"))
        self.assertEqual(result["minimum_executable_reference_notional"], D("10.71"))
        self.assertEqual(result["minimum_equity_satisfying_both"], D("49.266"))
        self.assertEqual(calculator.size_position(D("49.265"), settings)["quantity"], 0)
        self.assertEqual(calculator.size_position(D("49.266"), settings)["quantity"], D("0.21"))

    def test_cash_limit_with_fee_on_adversely_adjusted_entry(self):
        settings = {**self.sizing, "planned_risk_fraction": D("0.5")}
        result = calculator.size_position(100, settings)
        self.assertEqual(result["quantity"], D("1.99"))
        self.assertEqual(result["entry_cash_required"], D("99.64929975"))
        self.assertLessEqual(result["entry_cash_required"], 100)
        self.assertGreater(D("100") * D("1.0005") * D("1.001"), 100)
        minimum = calculator.minimum_executable_capital(settings)
        self.assertEqual(minimum["binding_constraint"], "entry_cash")
        self.assertEqual(minimum["minimum_equity_satisfying_both"], D("10.015005"))

    def test_planning_loss_is_conservative_for_stated_gap_free_cost_model(self):
        result = calculator.size_position(10000, self.sizing)
        exact_loss = result["reference_notional"] * (D("1.0005") * D("1.001") - D("0.98") * D("0.9995") * D("0.999"))
        self.assertEqual(result["modeled_gap_free_stop_loss_with_multiplicative_costs"], exact_loss)
        self.assertGreaterEqual(result["conservative_planned_gap_free_loss"], exact_loss)

    def test_monthly_break_even_and_higher_capital_identity(self):
        result = calculator.monthly_break_even(D("0.25"), 20, D("0.004"), D("0.003"), 15, 3000)
        self.assertEqual(result["break_even_equity"], 3000)
        self.assertEqual(result["linear_monthly_pnl_after_fixed_cost"], 0)
        self.assertEqual(result["assumed_gross_monthly_pnl"], 60)
        self.assertEqual(result["assumed_variable_monthly_friction"], 45)
        self.assertEqual(result["required_gross_notional_return_per_round_trip_for_break_even"], D("0.004"))

    def test_no_positive_break_even_for_nonpositive_surplus_with_fixed_cost(self):
        for gross in [D("0.001"), D("0.003")]:
            result = calculator.monthly_break_even(D("0.25"), 20, gross, D("0.003"), 15, 10000)
            self.assertIsNone(result["break_even_equity"])
            self.assertEqual(result["status"], "NO_POSITIVE_EQUITY_BREAK_EVEN")
            self.assertLess(result["linear_monthly_pnl_after_fixed_cost"], 0)
        zero = calculator.monthly_break_even(D("0.25"), 20, D("0.003"), D("0.003"), 0)
        self.assertEqual(zero["status"], "ALL_POSITIVE_EQUITIES_BREAK_EVEN_NO_OPERATING_SURPLUS")
        no_fixed = calculator.monthly_break_even(D("0.25"), 20, D("0.004"), D("0.003"), 0)
        self.assertEqual(no_fixed["break_even_equity"], 0)

    def test_income_arithmetic_and_loss_recovery_identity(self):
        expected = [203000, 101500, 50750]
        for rate, equity in zip([D("0.005"), D("0.01"), D("0.02")], expected):
            result = calculator.income_equity(1000, 15, rate)
            self.assertEqual(result["arithmetically_required_equity"], equity)
            self.assertEqual(D(equity) * rate - 15, 1000)
        for count in [10, 20, 50]:
            result = calculator.consecutive_losses(10000, D("0.005"), count, 50)
            remaining = result["fixed_fraction_remaining_equity"]
            self.assertEqual(remaining, D("10000") * D("0.995") ** count)
            self.assertAlmostEqual(remaining * (1 + result["fixed_fraction_recovery_return_required"]), D("10000"), places=25)
            self.assertGreater(remaining, result["fixed_dollar_remaining_equity"])

    def test_invalid_values_and_inconsistent_cost_definitions_rejected(self):
        for equity in [0, -1, D("NaN"), D("Infinity")]:
            with self.assertRaises(ValueError):
                calculator.size_position(equity, self.sizing)
        for amendment in [{"quantity_increment": 0}, {"planned_risk_fraction": D("1.1")}, {"stop_distance_fraction": 1}, {"linear_round_trip_friction_fraction": D("0.001")}]:
            with self.assertRaises(ValueError):
                calculator.size_position(100, {**self.sizing, **amendment})
        with self.assertRaises(ValueError):
            calculator.monthly_break_even(D("1.1"), 20, D("0.004"), D("0.003"), 15)
        with self.assertRaises(ValueError):
            calculator.income_equity(1000, 15, 0)

    def test_full_output_counts_and_currency_arithmetic_reconcile(self):
        directory = Path(__file__).resolve().parent / (".test-" + uuid4().hex)
        directory.mkdir()
        try:
            result = calculator.run(Path(__file__).resolve().parent / "protocol.json", directory)
            self.assertEqual(len(result["position_sizing"]), 4)
            self.assertEqual(len(result["monthly_cost_impact"]), 12)
            self.assertEqual(len(result["gross_return_sensitivity"]), 12)
            self.assertEqual(len(result["friction_sensitivity"]), 4)
            self.assertEqual(len(list(directory.glob("*.csv"))), 3)
            for row in result["monthly_cost_impact"]:
                self.assertEqual(row["assumed_gross_monthly_pnl"] - row["assumed_variable_monthly_friction"] - row["fixed_monthly_cost"], row["linear_monthly_pnl_after_fixed_cost"])
        finally:
            for path in directory.iterdir():
                path.unlink()
            directory.rmdir()


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