import pytest
from faker import Faker
from pandas.tseries.offsets import BDay
from rest_framework.reverse import reverse
from rest_framework.test import APIClient, APIRequestFactory
from wbcommission.analytics.marginality import MarginalityCalculator
from wbcommission.factories import CommissionTypeFactory, RebateFactory
from wbcommission.viewsets.rebate import RebateProductMarginalityViewSet
from wbportfolio.factories import FeesFactory, InstrumentPriceFactory, ProductFactory
from wbportfolio.models import Product

fake = Faker()


def _create_fixture(product, val_date, net_value=100, outstanding_shares=100):
    management = CommissionTypeFactory.create(key="management")
    perforance = CommissionTypeFactory.create(key="performance")
    i1 = InstrumentPriceFactory.create(
        instrument=product, net_value=net_value, outstanding_shares=outstanding_shares, calculated=False, date=val_date
    ).net_value  # create a price of AUM 100*100
    management_fees_1 = FeesFactory.create(
        linked_product=product, transaction_date=val_date, transaction_subtype="MANAGEMENT", calculated=False
    ).total_value
    performance_fees_1 = FeesFactory.create(
        linked_product=product, transaction_date=val_date, transaction_subtype="PERFORMANCE", calculated=False
    ).total_value
    performance_crys_fees_1 = FeesFactory.create(
        linked_product=product,
        transaction_date=val_date,
        transaction_subtype="PERFORMANCE_CRYSTALIZED",
        calculated=False,
    ).total_value
    management_rebate_1 = RebateFactory.create(product=product, date=val_date, commission_type=management).value
    performance_rebate_1 = RebateFactory.create(product=product, date=val_date, commission_type=perforance).value
    if val_date.weekday() == 0:
        return (
            i1,
            management_fees_1 / 3.0,
            performance_fees_1 / 3.0,
            performance_crys_fees_1 / 3.0,
            float(management_rebate_1) / 3.0,
            float(performance_rebate_1) / 3.0,
        )
    else:
        return (
            i1,
            management_fees_1,
            performance_fees_1,
            performance_crys_fees_1,
            float(management_rebate_1),
            float(performance_rebate_1),
        )


@pytest.mark.django_db
class TestMarginalityCalculator:
    @pytest.fixture
    def marginality_calculator(self):
        product = ProductFactory.create()
        start = (fake.date_object() + BDay(0)).date()
        end = (start + BDay(1)).date()

        (
            self.i1,
            self.management_fees_1,
            self.performance_fees_1,
            self.performance_crys_fees_1,
            self.management_rebate_1,
            self.performance_rebate_1,
        ) = _create_fixture(product, start)
        self.product_id = product.id
        self.start = start
        self.end = end
        return MarginalityCalculator(Product.objects.filter(id=product.id), start, end)

    def test_fees(self, marginality_calculator):
        assert marginality_calculator.management_fees.loc[self.product_id] == pytest.approx(
            self.management_fees_1, rel=1e-4
        )
        assert marginality_calculator.performance_fees.loc[self.product_id] == pytest.approx(
            self.performance_fees_1 + self.performance_crys_fees_1, rel=1e-4
        )
        assert marginality_calculator.total_fees.loc[self.product_id] == pytest.approx(
            self.performance_fees_1 + self.performance_crys_fees_1 + self.management_fees_1, rel=1e-4
        )

    def test_rebates(self, marginality_calculator):
        assert marginality_calculator.management_rebates.loc[self.product_id] == pytest.approx(
            self.management_rebate_1, rel=1e-4
        )
        assert marginality_calculator.performance_rebates.loc[self.product_id] == pytest.approx(
            self.performance_rebate_1, rel=1e-4
        )
        assert marginality_calculator.total_rebates.loc[self.product_id] == pytest.approx(
            self.performance_rebate_1 + self.management_rebate_1, rel=1e-4
        )

    def test_fees_usd(self, marginality_calculator):
        assert marginality_calculator.management_fees_usd.loc[self.product_id] == pytest.approx(
            self.management_fees_1, rel=1e-4
        )
        assert marginality_calculator.performance_fees_usd.loc[self.product_id] == pytest.approx(
            self.performance_fees_1 + self.performance_crys_fees_1, rel=1e-4
        )
        assert marginality_calculator.total_fees_usd.loc[self.product_id] == pytest.approx(
            self.performance_fees_1 + self.performance_crys_fees_1 + self.management_fees_1, rel=1e-4
        )

    def test_rebates_usd(self, marginality_calculator):
        assert marginality_calculator.management_rebates_usd.loc[self.product_id] == pytest.approx(
            self.management_rebate_1, rel=1e-4
        )
        assert marginality_calculator.performance_rebates_usd.loc[self.product_id] == pytest.approx(
            self.performance_rebate_1, rel=1e-4
        )
        assert marginality_calculator.total_rebates_usd.loc[self.product_id] == pytest.approx(
            self.performance_rebate_1 + self.management_rebate_1, rel=1e-4
        )

    def test_marginality(self, marginality_calculator):
        assert marginality_calculator.management_marginality.loc[self.product_id] == pytest.approx(
            self.management_fees_1 - self.management_rebate_1, rel=1e-4
        )
        assert marginality_calculator.performance_marginality.loc[self.product_id] == pytest.approx(
            self.performance_fees_1 + self.performance_crys_fees_1 - self.performance_rebate_1, rel=1e-4
        )
        assert marginality_calculator.total_marginality.loc[self.product_id] == pytest.approx(
            (self.performance_fees_1 + self.performance_crys_fees_1 + self.management_fees_1)
            - (self.performance_rebate_1 + self.management_rebate_1),
            rel=1e-4,
        )

    def test_marginality_usd(self, marginality_calculator):
        assert marginality_calculator.management_marginality_usd.loc[self.product_id] == pytest.approx(
            marginality_calculator.management_marginality.loc[self.product_id], rel=1e-4
        )
        assert marginality_calculator.performance_marginality_usd.loc[self.product_id] == pytest.approx(
            marginality_calculator.performance_marginality.loc[self.product_id], rel=1e-4
        )
        assert marginality_calculator.total_marginality_usd.loc[self.product_id] == pytest.approx(
            marginality_calculator.total_marginality.loc[self.product_id], rel=1e-4
        )

    def test_marginality_percent(self, marginality_calculator):
        assert marginality_calculator.management_marginality_percent.loc[self.product_id] == pytest.approx(
            (self.management_fees_1 - float(self.management_rebate_1)) / self.management_fees_1, rel=1e-4
        )
        assert marginality_calculator.performance_marginality_percent.loc[self.product_id] == pytest.approx(
            (self.performance_fees_1 + self.performance_crys_fees_1 - float(self.performance_rebate_1))
            / (self.performance_fees_1 + self.performance_crys_fees_1),
            rel=1e-4,
        )
        assert marginality_calculator.total_marginality_percent.loc[self.product_id] == pytest.approx(
            (
                (self.performance_fees_1 + self.performance_crys_fees_1 + self.management_fees_1)
                - (float(self.performance_rebate_1) + float(self.management_rebate_1))
            )
            / (self.performance_fees_1 + self.performance_crys_fees_1 + self.management_fees_1),
            rel=1e-4,
        )

    def test_marginality_percent_usd(self, marginality_calculator):
        assert marginality_calculator.management_marginality_percent_usd.loc[self.product_id] == pytest.approx(
            marginality_calculator.management_marginality_percent.loc[self.product_id], rel=1e-4
        )
        assert marginality_calculator.performance_marginality_percent_usd.loc[self.product_id] == pytest.approx(
            marginality_calculator.performance_marginality_percent.loc[self.product_id], rel=1e-4
        )
        assert marginality_calculator.total_marginality_percent_usd.loc[self.product_id] == pytest.approx(
            marginality_calculator.total_marginality_percent.loc[self.product_id], rel=1e-4
        )

    def test_get_net_marginality(self, marginality_calculator):
        assert marginality_calculator.get_net_marginality("management").loc[self.product_id] == pytest.approx(
            (self.management_fees_1 - float(self.management_rebate_1)) / (100 * 100) * 360, rel=1e-4
        )

    def test_get_aggregated_net_marginality(self, marginality_calculator):
        product_1 = ProductFactory.create()
        product_2 = ProductFactory.create()
        start = (fake.date_object() + BDay(0)).date()
        end = (start + BDay(1)).date()

        (
            i1,
            management_fees_1,
            performance_fees_1,
            performance_crys_fees_1,
            management_rebate_1,
            performance_rebate_1,
        ) = _create_fixture(product_1, start)
        (
            i2,
            management_fees_2,
            performance_fees_2,
            performance_crys_fees_2,
            management_rebate_2,
            performance_rebate_2,
        ) = _create_fixture(product_2, start, outstanding_shares=1000)
        total_aum = 100 * 100 + 100 * 1000
        calculator = MarginalityCalculator(Product.objects.filter(id__in=[product_1.id, product_2.id]), start, end)
        assert calculator.get_aggregated_net_marginality("management") == pytest.approx(
            ((management_fees_1 + management_fees_2) - (management_rebate_1 + management_rebate_2)) / total_aum * 360,
            rel=1e-4,
        )

    def test_rebate_marginality_view(self, marginality_calculator, super_user):
        request = APIRequestFactory().get("")
        request.query_params = {}
        request.GET = {"date_gte": self.start.strftime("%Y-%m-%d"), "date_lte": self.end.strftime("%Y-%m-%d")}
        request.user = super_user
        viewset = RebateProductMarginalityViewSet(request=request)
        assert set(viewset._get_dataframe().columns) == {
            "id",
            "title",
            "currency_symbol",
            "base_management_fees_percent",
            "management_fees",
            "management_rebates",
            "management_marginality",
            "management_marginality_percent",
            "base_performance_fees_percent",
            "performance_fees",
            "performance_rebates",
            "performance_marginality",
            "total_fees",
            "total_rebates",
            "total_marginality_percent",
            "total_fees_usd",
            "total_rebates_usd",
            "total_marginality_usd",
            "net_management_marginality",
            "net_performance_marginality",
        }

        # regression tests to check that without date range the view doesn't break
        url = reverse("wbcommission:rebatemarginalitytable-list", args=[])
        api_client = APIClient()
        api_client.force_authenticate(super_user)

        response = api_client.options(url)
        assert response.status_code == 200

        response = api_client.get(url)
        assert response.status_code == 200

        # check that with proper date range the viewset return some content
        response = api_client.options(url, data=request.GET)
        assert response.status_code == 200

        response = api_client.get(url, data=request.GET)
        assert response.status_code == 200
        assert response.json()["results"]
