import datetime
from decimal import Decimal

from NEMO.models import (
    Area,
    AreaAccessRecord,
    Consumable,
    ConsumableWithdraw,
    StaffCharge,
    Tool,
    TrainingSession,
    UsageEvent,
    User,
)
from NEMO.tests.test_utilities import NEMOTestCaseMixin
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from django.utils.timezone import make_aware

from NEMO_billing.invoices.models import InvoiceConfiguration
from NEMO_billing.invoices.processors import invoice_data_processor_class as data_processor
from NEMO_billing.rates.models import DailySchedule, Rate, RateTier, RateTime, RateType
from NEMO_billing.tests.test_utilities import basic_data
from NEMO_billing.utilities import round_decimal_amount


class TestRateTiers(NEMOTestCaseMixin, TestCase):

    def test_tool_usage_rate_with_tiers(self):
        test_user, test_project, tool, area = basic_data()

        tz = timezone.get_current_timezone()
        tool_usage_type = RateType.objects.get(type=RateType.Type.TOOL_USAGE)
        rate = Rate.objects.create(
            type=tool_usage_type, tool=tool, amount=Decimal("0.6"), daily=True, flat=True
        )  # 0.6 per hour = $0.01 per minute
        rate.full_clean()
        tool_access_start = make_aware(
            datetime.datetime(year=2022, month=2, day=21, hour=17), tz
        )  # Mon Feb 21 2022 at 5PM
        tool_access_end = make_aware(
            datetime.datetime(year=2022, month=2, day=25, hour=3), tz
        )  # Fri Feb 25 2022 at 3AM
        diff_minutes = (tool_access_end - tool_access_start).total_seconds() / 60
        # Create tool usage
        UsageEvent.objects.create(
            user=test_user,
            operator=test_user,
            project=test_project,
            tool=tool,
            start=tool_access_start,
            end=tool_access_end,
        )

        rate_tier_one = RateTier(rate=rate, quantity=120, amount=6)  # 6 per hour = 0.1 per minute after 2 hours
        # Cannot have tier on daily rates
        self.assertRaises(ValidationError, rate_tier_one.full_clean)

        rate.daily = False
        rate.flat = False
        rate.save()
        rate_tier_one.save()

        billing_start = make_aware(datetime.datetime(year=2022, month=2, day=1), tz)
        billing_end = make_aware(datetime.datetime(year=2022, month=2, day=28), tz)
        config = InvoiceConfiguration.first_or_default()

        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 0.01 * 120 + (diff_minutes - 120) * 0.1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # add another tier
        rate_tier_two = RateTier.objects.create(rate=rate, quantity=180, amount=60)  # 60 flat after 3 hours
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 0.01 * 120 + 60 * 0.1 + (diff_minutes - 180) * 1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # make the first tier flat
        rate_tier_one.flat = True
        rate_tier_one.save()
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 0.01 * 120 + 6 + (diff_minutes - 180) * 1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # make that last rate flat, set back first tier
        rate_tier_one.flat = False
        rate_tier_one.save()
        rate_tier_two.flat = True
        rate_tier_two.save()
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 0.01 * 120 + 60 * 0.1 + 60
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # test with timed rate, which is not allowed if we have tiers
        rate_time = RateTime.objects.create(name="rate time 1")
        DailySchedule.objects.create(
            rate_time=rate_time, start_time=datetime.time(0, 0, 0), end_time=datetime.time(1, 0, 0), weekday=1
        )
        # for existing rate
        rate.time = rate_time
        self.assertRaises(ValidationError, rate.full_clean)
        # and also for new rate
        new_rate = Rate(type=tool_usage_type, tool=tool, time=rate_time, amount=120)
        self.assertRaises(ValidationError, new_rate.full_clean)

        # if it's for a different tool, it works
        tool_2 = Tool.objects.create(name="tool 2")
        rate_2 = Rate(type=tool_usage_type, tool=tool_2, time=rate_time, amount=120)
        rate_2.full_clean()
        rate_2.save()
        # until we try to add a tier
        rate_tier = RateTier(rate=rate_2, quantity=120, amount=6)  # 6 per hour = 0.1 per minute after 2 hours
        # Cannot have tier on rates with times
        self.assertRaises(ValidationError, rate_tier.full_clean)

    def test_area_rate_with_tiers(self):
        test_user, test_project, tool, area = basic_data()

        tz = timezone.get_current_timezone()
        area_access_type = RateType.objects.get(type=RateType.Type.AREA_USAGE)
        rate = Rate.objects.create(
            type=area_access_type, area=area, amount=Decimal("0.6"), daily=True, flat=True
        )  # 0.6 per hour = $0.01 per minute
        rate.full_clean()
        area_access_start = make_aware(
            datetime.datetime(year=2022, month=2, day=21, hour=17), tz
        )  # Mon Feb 21 2022 at 5PM
        area_access_end = make_aware(
            datetime.datetime(year=2022, month=2, day=25, hour=3), tz
        )  # Fri Feb 25 2022 at 3AM
        diff_minutes = (area_access_end - area_access_start).total_seconds() / 60
        # Create area usage
        AreaAccessRecord.objects.create(
            customer=test_user, project=test_project, area=area, start=area_access_start, end=area_access_end
        )

        rate_tier_one = RateTier(
            rate=rate, quantity=120, amount=Decimal("6")
        )  # 6 per hour = 0.1 per minute after 2 hours
        # Cannot have tier on daily rates
        self.assertRaises(ValidationError, rate_tier_one.full_clean)

        rate.daily = False
        rate.flat = False
        rate.save()
        rate_tier_one.save()

        billing_start = make_aware(datetime.datetime(year=2022, month=2, day=1), tz)
        billing_end = make_aware(datetime.datetime(year=2022, month=2, day=28), tz)
        config = InvoiceConfiguration.first_or_default()

        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 0.01 * 120 + (diff_minutes - 120) * 0.1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # add another tier
        rate_tier_two = RateTier.objects.create(rate=rate, quantity=180, amount=60)  # 60 flat after 3 hours
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 0.01 * 120 + 60 * 0.1 + (diff_minutes - 180) * 1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # make the first tier flat
        rate_tier_one.flat = True
        rate_tier_one.save()
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 0.01 * 120 + 6 + (diff_minutes - 180) * 1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # make that last rate flat, set back first tier
        rate_tier_one.flat = False
        rate_tier_one.save()
        rate_tier_two.flat = True
        rate_tier_two.save()
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 0.01 * 120 + 60 * 0.1 + 60
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # test with timed rate, which is not allowed if we have tiers
        rate_time = RateTime.objects.create(name="rate time 1")
        DailySchedule.objects.create(
            rate_time=rate_time, start_time=datetime.time(0, 0, 0), end_time=datetime.time(1, 0, 0), weekday=1
        )
        # for existing rate
        rate.time = rate_time
        self.assertRaises(ValidationError, rate.full_clean)
        # and also for new rate
        new_rate = Rate(type=area_access_type, area=area, time=rate_time, amount=120)
        self.assertRaises(ValidationError, new_rate.full_clean)

        # if it's for a different area, it works
        area_2 = Area.objects.create(name="area 2")
        rate_2 = Rate(type=area_access_type, area=area_2, time=rate_time, amount=120)
        rate_2.full_clean()
        rate_2.save()
        # until we try to add a tier
        rate_tier = RateTier(rate=rate_2, quantity=120, amount=6)  # 6 per hour = 0.1 per minute after 2 hours
        # Cannot have tier on rates with times
        self.assertRaises(ValidationError, rate_tier.full_clean)

    def test_supply_rate(self):
        test_user, test_project, tool, area = basic_data()
        staff_member = User.objects.create(
            username="staff", first_name="Staff", last_name="Staff", is_staff=True, badge_number=2
        )
        supply = Consumable.objects.create(name="Supply", quantity=1, reusable=True)

        tz = timezone.get_current_timezone()
        consumable_type = RateType.objects.get(type=RateType.Type.CONSUMABLE)
        rate = Rate.objects.create(type=consumable_type, amount=60, consumable=supply, flat=True)  # 60 per supply
        rate.full_clean()
        order_date = make_aware(datetime.datetime(year=2022, month=2, day=25, hour=3), tz)  # Fri Feb 25 2022 at 3AM
        # Create order
        quantity = 10
        ConsumableWithdraw.objects.create(
            merchant=staff_member,
            customer=test_user,
            project=test_project,
            consumable=supply,
            date=order_date,
            quantity=quantity,
        )

        rate_tier_one = RateTier(rate=rate, quantity=5, amount=6)  # 6 per supply after 5 units
        # has to be flat
        self.assertRaises(ValidationError, rate_tier_one.full_clean)
        rate_tier_one.flat = True
        rate_tier_one.save()

        billing_start = make_aware(datetime.datetime(year=2022, month=2, day=1), tz)
        billing_end = make_aware(datetime.datetime(year=2022, month=2, day=28), tz)
        config = InvoiceConfiguration.first_or_default()

        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 60 * 5 + (quantity - 5) * 6
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # add another tier
        rate_tier_two = RateTier.objects.create(rate=rate, quantity=7, amount=600)  # 60 after 7
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 60 * 5 + 6 * 2 + (quantity - 7) * 600
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # test with timed rate, which is not allowed if we have tiers
        rate_time = RateTime.objects.create(name="rate time 1")
        DailySchedule.objects.create(
            rate_time=rate_time, start_time=datetime.time(0, 0, 0), end_time=datetime.time(1, 0, 0), weekday=1
        )
        # for existing rate
        rate.time = rate_time
        self.assertRaises(ValidationError, rate.full_clean)
        # and also for new rate
        new_rate = Rate(type=consumable_type, consumable=supply, time=rate_time, amount=120)
        self.assertRaises(ValidationError, new_rate.full_clean)

    def test_staff_charge_rate_with_tiers(self):
        test_user, test_project, tool, area = basic_data()
        staff_member = User.objects.create(
            username="staff", first_name="Staff", last_name="Staff", is_staff=True, badge_number=2
        )

        tz = timezone.get_current_timezone()
        staff_charge_type = RateType.objects.get(type=RateType.Type.STAFF_CHARGE)
        rate = Rate.objects.create(type=staff_charge_type, amount=60)  # 60 per hour = $1 per minute
        rate.full_clean()
        staff_charge_start = make_aware(
            datetime.datetime(year=2022, month=2, day=21, hour=17), tz
        )  # Mon Feb 21 2022 at 5PM
        staff_charge_end = make_aware(
            datetime.datetime(year=2022, month=2, day=25, hour=3), tz
        )  # Fri Feb 25 2022 at 3AM
        diff_minutes = (staff_charge_end - staff_charge_start).total_seconds() / 60
        # Create staff charge
        StaffCharge.objects.create(
            staff_member=staff_member,
            customer=test_user,
            project=test_project,
            start=staff_charge_start,
            end=staff_charge_end,
        )

        rate_tier_one = RateTier(rate=rate, quantity=120, amount=6)  # 6 per hour = 0.1 per minute after 2 hours
        rate_tier_one.full_clean()
        rate_tier_one.save()

        billing_start = make_aware(datetime.datetime(year=2022, month=2, day=1), tz)
        billing_end = make_aware(datetime.datetime(year=2022, month=2, day=28), tz)
        config = InvoiceConfiguration.first_or_default()

        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 120 + (diff_minutes - 120) * 0.1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # add another tier
        rate_tier_two = RateTier.objects.create(rate=rate, quantity=180, amount=60)  # 60 flat after 3 hours
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 120 + 60 * 0.1 + (diff_minutes - 180) * 1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # make the first tier flat
        rate_tier_one.flat = True
        rate_tier_one.save()
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 120 + 6 + (diff_minutes - 180) * 1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # make that last rate flat, set back first tier
        rate_tier_one.flat = False
        rate_tier_one.save()
        rate_tier_two.flat = True
        rate_tier_two.save()
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 120 + 60 * 0.1 + 60
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # test with timed rate, which is not allowed if we have tiers
        rate_time = RateTime.objects.create(name="rate time 1")
        DailySchedule.objects.create(
            rate_time=rate_time, start_time=datetime.time(0, 0, 0), end_time=datetime.time(1, 0, 0), weekday=1
        )
        # for existing rate
        rate.time = rate_time
        self.assertRaises(ValidationError, rate.full_clean)

    def test_training_individual_rate_with_tiers(self):
        test_user, test_project, tool, area = basic_data()
        staff_member = User.objects.create(
            username="staff", first_name="Staff", last_name="Staff", is_staff=True, badge_number=2
        )

        tz = timezone.get_current_timezone()
        training_individual_type = RateType.objects.get(type=RateType.Type.TOOL_TRAINING_INDIVIDUAL)
        rate = Rate.objects.create(type=training_individual_type, amount=60, tool=tool, daily=True)  # 60 per hour
        training_date = make_aware(datetime.datetime(year=2022, month=2, day=25, hour=3), tz)  # Fri Feb 25 2022 at 3AM
        # Create 90-min training session training
        duration = 90
        TrainingSession.objects.create(
            type=TrainingSession.Type.INDIVIDUAL,
            trainer=staff_member,
            trainee=test_user,
            project=test_project,
            tool=tool,
            date=training_date,
            duration=duration,
        )

        rate_tier_one = RateTier(
            rate=rate, quantity=15, amount=Decimal("6")
        )  # 6 per hour = 0.1 per minute after 15 min
        # Cannot have tier on daily rates
        self.assertRaises(ValidationError, rate_tier_one.full_clean)

        rate.daily = False
        rate.flat = False
        rate.save()
        rate_tier_one.save()

        billing_start = make_aware(datetime.datetime(year=2022, month=2, day=1), tz)
        billing_end = make_aware(datetime.datetime(year=2022, month=2, day=28), tz)
        config = InvoiceConfiguration.first_or_default()

        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 15 + (duration - 15) * 0.1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # add another tier
        rate_tier_two = RateTier.objects.create(rate=rate, quantity=40, amount=60)  # 60 per hour after 40 min
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 15 + 25 * 0.1 + (duration - 40) * 1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # make the first tier flat
        rate_tier_one.flat = True
        rate_tier_one.save()
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 15 + 6 + (duration - 40) * 1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # make that last rate flat, set back first tier
        rate_tier_one.flat = False
        rate_tier_one.save()
        rate_tier_two.flat = True
        rate_tier_two.save()
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 15 + 25 * 0.1 + 60
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # test with timed rate, which is not allowed if we have tiers
        rate_time = RateTime.objects.create(name="rate time 1")
        DailySchedule.objects.create(
            rate_time=rate_time, start_time=datetime.time(0, 0, 0), end_time=datetime.time(1, 0, 0), weekday=1
        )
        # for existing rate
        rate.time = rate_time
        self.assertRaises(ValidationError, rate.full_clean)
        # and also for new rate
        new_rate = Rate(type=training_individual_type, tool=tool, time=rate_time, amount=120)
        self.assertRaises(ValidationError, new_rate.full_clean)

    def test_training_group_rate_with_tiers(self):
        test_user, test_project, tool, area = basic_data()
        staff_member = User.objects.create(
            username="staff", first_name="Staff", last_name="Staff", is_staff=True, badge_number=2
        )

        tz = timezone.get_current_timezone()
        training_individual_type = RateType.objects.get(type=RateType.Type.TOOL_TRAINING_GROUP)
        rate = Rate.objects.create(type=training_individual_type, amount=60, tool=tool, daily=True)  # 60 per hour
        training_date = make_aware(datetime.datetime(year=2022, month=2, day=25, hour=3), tz)  # Fri Feb 25 2022 at 3AM
        # Create 90-min training session training
        duration = 90
        TrainingSession.objects.create(
            type=TrainingSession.Type.GROUP,
            trainer=staff_member,
            trainee=test_user,
            project=test_project,
            tool=tool,
            date=training_date,
            duration=duration,
        )

        rate_tier_one = RateTier(
            rate=rate, quantity=15, amount=Decimal("6")
        )  # 6 per hour = 0.1 per minute after 15 min
        # Cannot have tier on daily rates
        self.assertRaises(ValidationError, rate_tier_one.full_clean)

        rate.daily = False
        rate.flat = False
        rate.save()
        rate_tier_one.save()

        billing_start = make_aware(datetime.datetime(year=2022, month=2, day=1), tz)
        billing_end = make_aware(datetime.datetime(year=2022, month=2, day=28), tz)
        config = InvoiceConfiguration.first_or_default()

        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 15 + (duration - 15) * 0.1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # add another tier
        rate_tier_two = RateTier.objects.create(rate=rate, quantity=40, amount=60)  # 60 per hour after 40 min
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 15 + 25 * 0.1 + (duration - 40) * 1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # make the first tier flat
        rate_tier_one.flat = True
        rate_tier_one.save()
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 15 + 6 + (duration - 40) * 1
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # make that last rate flat, set back first tier
        rate_tier_one.flat = False
        rate_tier_one.save()
        rate_tier_two.flat = True
        rate_tier_two.save()
        billables = data_processor.get_billable_items(billing_start, billing_end, config)
        total = 1 * 15 + 25 * 0.1 + 60
        self.assertEqual(len(billables), 1)
        self.assertEqual(sum([billable.amount for billable in billables]), round_decimal_amount(total))

        # test with timed rate, which is not allowed if we have tiers
        rate_time = RateTime.objects.create(name="rate time 1")
        DailySchedule.objects.create(
            rate_time=rate_time, start_time=datetime.time(0, 0, 0), end_time=datetime.time(1, 0, 0), weekday=1
        )
        # for existing rate
        rate.time = rate_time
        self.assertRaises(ValidationError, rate.full_clean)
        # and also for new rate
        new_rate = Rate(type=training_individual_type, tool=tool, time=rate_time, amount=120)
        self.assertRaises(ValidationError, new_rate.full_clean)
