"""Tests for investment start_date logic and reward calculation timing."""

from __future__ import annotations

from datetime import date, timedelta
from decimal import Decimal

from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APIClient, APITestCase

from apps.investments.cron_tasks import CalculateRewardsTask
from apps.investments.models import Investment, Reward
from apps.users.models import User


class InvestmentStartDateTests(APITestCase):
    """Standard tests for investment start_date being created_at + 1 day."""

    def setUp(self) -> None:
        """Set up test user with sufficient balance."""
        self.user = User.objects.create_user(
            telegram_user_id=5001,
            first_name="TestUser",
            username="testuser",
        )
        self.user.credit_balance = Decimal("20000.00000000")  # Increased to cover all test cases
        self.user.save()
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)

    def test_start_date_is_next_day_after_creation(self):
        """Standard: start_date is created_at + 1 day."""
        url = reverse("investment-list")
        before_creation = timezone.now()
        
        response = self.client.post(url, {"amount": "1000"}, format="json")
        
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        investment = Investment.objects.get(id=response.data["id"])
        
        # start_date should be approximately 1 day after created_at
        expected_start = investment.created_at + timedelta(days=1)
        time_diff = abs((investment.start_date - expected_start).total_seconds())
        self.assertLess(time_diff, 1, "start_date should be exactly created_at + 1 day")
        
        # Verify start_date is after created_at
        self.assertGreater(investment.start_date, investment.created_at)

    def test_end_date_is_start_date_plus_duration(self):
        """Standard: end_date = start_date + duration_days."""
        url = reverse("investment-list")
        response = self.client.post(url, {"amount": "1000"}, format="json")
        
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        investment = Investment.objects.get(id=response.data["id"])
        
        # end_date should be start_date + duration_days
        expected_end = investment.start_date + timedelta(days=investment.duration_days)
        time_diff = abs((investment.end_date - expected_end).total_seconds())
        self.assertLess(time_diff, 1, "end_date should be start_date + duration_days")

    def test_multiple_investments_same_day_independent_start_dates(self):
        """Standard: Multiple investments created same day each get start_date = next day."""
        url = reverse("investment-list")
        
        # Create 3 investments in quick succession
        response1 = self.client.post(url, {"amount": "100"}, format="json")
        response2 = self.client.post(url, {"amount": "500"}, format="json")
        response3 = self.client.post(url, {"amount": "1000"}, format="json")
        
        inv1 = Investment.objects.get(id=response1.data["id"])
        inv2 = Investment.objects.get(id=response2.data["id"])
        inv3 = Investment.objects.get(id=response3.data["id"])
        
        # All should have start_date = created_at + 1 day
        for inv in [inv1, inv2, inv3]:
            expected_start = inv.created_at + timedelta(days=1)
            time_diff = abs((inv.start_date - expected_start).total_seconds())
            self.assertLess(time_diff, 1)

    def test_different_tiers_same_start_date_logic(self):
        """Standard: All tiers use same start_date logic regardless of amount."""
        url = reverse("investment-list")
        
        # Test tier 1, 3, and 6
        amounts = ["50", "1000", "10000"]
        expected_tiers = [1, 3, 6]
        
        for amount, expected_tier in zip(amounts, expected_tiers):
            response = self.client.post(url, {"amount": amount}, format="json")
            self.assertEqual(response.status_code, status.HTTP_201_CREATED, 
                           f"Failed to create investment with amount {amount}: {response.data}")
            inv = Investment.objects.get(id=response.data["id"])
            
            self.assertEqual(inv.tier, expected_tier)
            expected_start = inv.created_at + timedelta(days=1)
            time_diff = abs((inv.start_date - expected_start).total_seconds())
            self.assertLess(time_diff, 1)


class InvestmentStartDateEdgeCaseTests(APITestCase):
    """Edge case tests for investment start_date logic."""

    def setUp(self) -> None:
        """Set up test user with sufficient balance."""
        self.user = User.objects.create_user(
            telegram_user_id=5002,
            first_name="EdgeUser",
            username="edgeuser",
        )
        self.user.credit_balance = Decimal("10000.00000000")
        self.user.save()
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)

    def test_investment_created_at_midnight(self):
        """Edge: Investment created at exactly 00:00:00."""
        # Manually create investment at midnight
        midnight = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
        tier_info = Investment.get_tier_info(Decimal("1000"))
        
        investment = Investment.objects.create(
            user=self.user,
            amount=Decimal("1000"),
            tier=tier_info["tier"],
            daily_reward_rate=tier_info["rate"],
            duration_days=tier_info["duration"],
            status=Investment.STATUS_PENDING,
            start_date=midnight + timedelta(days=1),
            end_date=midnight + timedelta(days=1) + timedelta(days=tier_info["duration"]),
        )
        investment.created_at = midnight
        investment.save()
        
        # start_date should be next day at 00:00:00
        expected_start = midnight + timedelta(days=1)
        self.assertEqual(investment.start_date.date(), expected_start.date())

    def test_investment_created_just_before_midnight(self):
        """Edge: Investment created at 23:59:59."""
        # Create investment via API (uses timezone.now())
        url = reverse("investment-list")
        response = self.client.post(url, {"amount": "1000"}, format="json")
        
        investment = Investment.objects.get(id=response.data["id"])
        
        # Regardless of creation time, start_date is always created_at + 1 day
        expected_start = investment.created_at + timedelta(days=1)
        time_diff = abs((investment.start_date - expected_start).total_seconds())
        self.assertLess(time_diff, 1)
        
        # Verify dates are different days
        self.assertNotEqual(investment.created_at.date(), investment.start_date.date())

    def test_minimum_investment_amount_start_date(self):
        """Edge: Minimum investment ($10) uses same start_date logic."""
        url = reverse("investment-list")
        self.user.credit_balance = Decimal("10.00000000")
        self.user.save()
        
        response = self.client.post(url, {"amount": "10"}, format="json")
        
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        investment = Investment.objects.get(id=response.data["id"])
        
        expected_start = investment.created_at + timedelta(days=1)
        time_diff = abs((investment.start_date - expected_start).total_seconds())
        self.assertLess(time_diff, 1)

    def test_maximum_tier_investment_start_date(self):
        """Edge: Large investment (tier 6) uses same start_date logic."""
        url = reverse("investment-list")
        response = self.client.post(url, {"amount": "10000"}, format="json")
        
        investment = Investment.objects.get(id=response.data["id"])
        self.assertEqual(investment.tier, 6)
        
        expected_start = investment.created_at + timedelta(days=1)
        time_diff = abs((investment.start_date - expected_start).total_seconds())
        self.assertLess(time_diff, 1)


class RewardCalculationTimingTests(TestCase):
    """Standard tests for reward calculation based on start_date."""

    def setUp(self) -> None:
        """Set up test user and investment."""
        self.user = User.objects.create_user(
            telegram_user_id=5003,
            first_name="RewardUser",
            username="rewarduser",
        )
        self.user.credit_balance = Decimal("10000.00000000")
        self.user.save()

    def test_no_reward_on_creation_day(self):
        """Standard: No rewards calculated for investment on its creation day."""
        # Investment created on Dec 20, start_date = Dec 21
        now = timezone.now()
        creation_date = now.replace(hour=12, minute=0, second=0, microsecond=0)
        
        tier_info = Investment.get_tier_info(Decimal("1000"))
        investment = Investment.objects.create(
            user=self.user,
            amount=Decimal("1000"),
            tier=tier_info["tier"],
            daily_reward_rate=tier_info["rate"],
            duration_days=tier_info["duration"],
            status=Investment.STATUS_PENDING,
            start_date=creation_date + timedelta(days=1),
            end_date=creation_date + timedelta(days=1) + timedelta(days=tier_info["duration"]),
        )
        investment.created_at = creation_date
        investment.save()
        
        # Run reward calculation for creation day (Dec 20)
        target_date = creation_date.date()
        task = CalculateRewardsTask(target_date)
        count = task._calculate_pending_rewards(target_date)
        
        # Should create 0 rewards (start_date Dec 21 > target_date Dec 20)
        self.assertEqual(count, 0)
        self.assertEqual(Reward.objects.filter(investment=investment).count(), 0)

    def test_first_reward_on_start_date(self):
        """Standard: First reward calculated the day AFTER start_date."""
        # Investment created on Dec 20, start_date = Dec 21
        now = timezone.now()
        creation_date = now.replace(hour=12, minute=0, second=0, microsecond=0)
        start_date = creation_date + timedelta(days=1)
        
        tier_info = Investment.get_tier_info(Decimal("1000"))
        investment = Investment.objects.create(
            user=self.user,
            amount=Decimal("1000"),
            tier=tier_info["tier"],
            daily_reward_rate=tier_info["rate"],
            duration_days=tier_info["duration"],
            status=Investment.STATUS_PENDING,
            start_date=start_date,
            end_date=start_date + timedelta(days=tier_info["duration"]),
        )
        investment.created_at = creation_date
        investment.save()
        
        # Run reward calculation for start_date (Dec 21) - should create 0 rewards
        target_date = start_date.date()
        task = CalculateRewardsTask(target_date)
        count = task._calculate_pending_rewards(target_date)
        self.assertEqual(count, 0)
        
        # Run reward calculation for start_date + 1 (Dec 22) - should create 1 reward
        target_date = (start_date + timedelta(days=1)).date()
        task = CalculateRewardsTask(target_date)
        count = task._calculate_pending_rewards(target_date)
        self.assertEqual(count, 1)
        reward = Reward.objects.get(investment=investment)
        self.assertEqual(reward.reward_date, target_date)
        self.assertEqual(reward.amount, Decimal("20.00000000"))  # 2% of 1000

    def test_reward_calculation_workflow(self):
        """Standard: Complete workflow from creation through multiple days."""
        # Day 0: Investment created on Dec 20 at 12:00 PM
        base_time = timezone.now().replace(hour=12, minute=0, second=0, microsecond=0)
        tier_info = Investment.get_tier_info(Decimal("1000"))
        
        investment = Investment.objects.create(
            user=self.user,
            amount=Decimal("1000"),
            tier=tier_info["tier"],
            daily_reward_rate=tier_info["rate"],
            duration_days=tier_info["duration"],
            status=Investment.STATUS_PENDING,
            start_date=base_time + timedelta(days=1),
            end_date=base_time + timedelta(days=1) + timedelta(days=tier_info["duration"]),
        )
        investment.created_at = base_time
        investment.save()
        
        # Day 1: Run for Dec 20 (creation day) - should create 0 rewards
        task = CalculateRewardsTask(base_time.date())
        count = task._calculate_pending_rewards(base_time.date())
        self.assertEqual(count, 0)
        
        # Day 2: Run for Dec 21 (start_date) - should create 0 rewards (first reward is day after start_date)
        day2_date = (base_time + timedelta(days=1)).date()
        task = CalculateRewardsTask(day2_date)
        count = task._calculate_pending_rewards(day2_date)
        self.assertEqual(count, 0)
        self.assertEqual(Reward.objects.filter(investment=investment).count(), 0)
        
        # Day 3: Run for Dec 22 (start_date + 1) - should create 1 reward
        day3_date = (base_time + timedelta(days=2)).date()
        task = CalculateRewardsTask(day3_date)
        count = task._calculate_pending_rewards(day3_date)
        self.assertEqual(count, 1)
        self.assertEqual(Reward.objects.filter(investment=investment).count(), 1)

    def test_multiple_investments_correct_timing(self):
        """Standard: Multiple investments created on different days calculate correctly."""
        base_time = timezone.now().replace(hour=12, minute=0, second=0, microsecond=0)
        
        # Investment 1: Created Dec 20, start_date Dec 21
        tier_info = Investment.get_tier_info(Decimal("500"))
        inv1 = Investment.objects.create(
            user=self.user,
            amount=Decimal("500"),
            tier=tier_info["tier"],
            daily_reward_rate=tier_info["rate"],
            duration_days=tier_info["duration"],
            status=Investment.STATUS_PENDING,
            start_date=base_time + timedelta(days=1),
            end_date=base_time + timedelta(days=1) + timedelta(days=tier_info["duration"]),
        )
        inv1.created_at = base_time
        inv1.save()
        
        # Investment 2: Created Dec 22, start_date Dec 23
        tier_info2 = Investment.get_tier_info(Decimal("1000"))
        inv2 = Investment.objects.create(
            user=self.user,
            amount=Decimal("1000"),
            tier=tier_info2["tier"],
            daily_reward_rate=tier_info2["rate"],
            duration_days=tier_info2["duration"],
            status=Investment.STATUS_PENDING,
            start_date=base_time + timedelta(days=3),
            end_date=base_time + timedelta(days=3) + timedelta(days=tier_info2["duration"]),
        )
        inv2.created_at = base_time + timedelta(days=2)
        inv2.save()
        
        # Run for Dec 21: Neither investment should have rewards yet
        # inv1 start_date is Dec 21, so first reward is Dec 22
        day1 = (base_time + timedelta(days=1)).date()
        task = CalculateRewardsTask(day1)
        task._calculate_pending_rewards(day1)
        self.assertEqual(Reward.objects.filter(investment=inv1).count(), 0)
        self.assertEqual(Reward.objects.filter(investment=inv2).count(), 0)
        
        # Run for Dec 22: inv1 should have 1 reward (Dec 22), inv2 none yet
        day2 = (base_time + timedelta(days=2)).date()
        task = CalculateRewardsTask(day2)
        task._calculate_pending_rewards(day2)
        self.assertEqual(Reward.objects.filter(investment=inv1).count(), 1)  # Dec 22
        self.assertEqual(Reward.objects.filter(investment=inv2).count(), 0)
        
        # Run for Dec 23: inv1 should have 2 rewards (Dec 22, 23), inv2 none yet
        # inv2 start_date is Dec 23, so first reward is Dec 24
        day3 = (base_time + timedelta(days=3)).date()
        task = CalculateRewardsTask(day3)
        task._calculate_pending_rewards(day3)
        self.assertEqual(Reward.objects.filter(investment=inv1).count(), 2)  # Dec 22, 23
        self.assertEqual(Reward.objects.filter(investment=inv2).count(), 0)  # First reward Dec 24


class RewardCalculationEdgeCaseTests(TestCase):
    """Edge case tests for reward calculation timing."""

    def setUp(self) -> None:
        """Set up test user."""
        self.user = User.objects.create_user(
            telegram_user_id=5004,
            first_name="EdgeRewardUser",
            username="edgerewarduser",
        )
        self.user.credit_balance = Decimal("10000.00000000")
        self.user.save()

    def test_investment_expiring_on_last_day(self):
        """Edge: Investment with 1-day duration gets 1 reward on the day after start_date."""
        now = timezone.now()
        tier_info = Investment.get_tier_info(Decimal("1000"))
        
        # Create investment with duration of 1 day only
        # start_date = Dec 21, end_date = Dec 22
        # First (and only) reward should be on Dec 22
        investment = Investment.objects.create(
            user=self.user,
            amount=Decimal("1000"),
            tier=tier_info["tier"],
            daily_reward_rate=tier_info["rate"],
            duration_days=1,
            status=Investment.STATUS_PENDING,
            start_date=now,
            end_date=now + timedelta(days=1),
        )
        
        # Run for start_date - should create 0 rewards
        target_date = now.date()
        task = CalculateRewardsTask(target_date)
        count = task._calculate_pending_rewards(target_date)
        self.assertEqual(count, 0)
        
        # Run for end_date (start_date + 1) - should create 1 reward
        end_target_date = (now + timedelta(days=1)).date()
        task = CalculateRewardsTask(end_target_date)
        count = task._calculate_pending_rewards(end_target_date)
        self.assertEqual(count, 1)
        
    def test_investment_beyond_end_date_no_reward(self):
        """Edge: Investment past end_date doesn't generate rewards."""
        now = timezone.now()
        past_date = now - timedelta(days=10)
        tier_info = Investment.get_tier_info(Decimal("1000"))
        
        # Create investment that already ended
        investment = Investment.objects.create(
            user=self.user,
            amount=Decimal("1000"),
            tier=tier_info["tier"],
            daily_reward_rate=tier_info["rate"],
            duration_days=5,
            status=Investment.STATUS_ACTIVE,
            start_date=past_date,
            end_date=past_date + timedelta(days=5),
        )
        
        # Try to calculate reward for today (after end_date)
        target_date = now.date()
        task = CalculateRewardsTask(target_date)
        count = task._calculate_pending_rewards(target_date)
        
        # Should create 0 rewards (past end_date)
        self.assertEqual(count, 0)

    def test_start_date_equals_target_date_boundary(self):
        """Edge: Boundary condition where start_date.date() == target_date - no reward yet."""
        now = timezone.now()
        # Set start_date to exactly midnight of target date
        start_datetime = now.replace(hour=0, minute=0, second=0, microsecond=0)
        target_date = start_datetime.date()
        
        tier_info = Investment.get_tier_info(Decimal("1000"))
        investment = Investment.objects.create(
            user=self.user,
            amount=Decimal("1000"),
            tier=tier_info["tier"],
            daily_reward_rate=tier_info["rate"],
            duration_days=tier_info["duration"],
            status=Investment.STATUS_PENDING,
            start_date=start_datetime,
            end_date=start_datetime + timedelta(days=tier_info["duration"]),
        )
        
        # Run for target_date (equals start_date) - should create 0 rewards
        task = CalculateRewardsTask(target_date)
        count = task._calculate_pending_rewards(target_date)
        self.assertEqual(count, 0)
        
        # Run for target_date + 1 - should create 1 reward
        next_date = (start_datetime + timedelta(days=1)).date()
        task = CalculateRewardsTask(next_date)
        count = task._calculate_pending_rewards(next_date)
        self.assertEqual(count, 1)
        reward = Reward.objects.get(investment=investment)
        self.assertEqual(reward.reward_date, next_date)
