"""
Tests for MarkiTect Period Management Framework.

This module tests the complete period lifecycle management system including:
- Period creation, status management, and lifecycle transitions
- Period overlap validation and conflict resolution
- Period calculations and cost aggregation
- Period closure validation and audit trails
- Current period detection and auto-creation
"""

import pytest
import tempfile
import os
from datetime import date, datetime, timedelta
from decimal import Decimal

from markitect.finance.period_manager import PeriodManager, PeriodStatus, Period
from markitect.finance.models import FinanceModels
from markitect.finance.cost_manager import CostItemManager, CostItem


class TestPeriodManager:
    """Test suite for period management system."""

    @pytest.fixture
    def temp_db(self):
        """Create temporary database for testing."""
        fd, path = tempfile.mkstemp(suffix='.db')
        os.close(fd)
        yield path
        os.unlink(path)

    @pytest.fixture
    def period_manager(self, temp_db):
        """Create period manager with initialized database."""
        finance_models = FinanceModels(temp_db)
        finance_models.initialize_finance_schema()
        return PeriodManager(temp_db)

    @pytest.fixture
    def sample_period_data(self):
        """Sample period data for testing."""
        return {
            'period_start': date(2025, 1, 1),
            'period_end': date(2025, 1, 31),
            'period_type': 'monthly'
        }

    def test_period_status_enum(self):
        """Test period status enumeration."""
        assert PeriodStatus.OPEN.value == 'open'
        assert PeriodStatus.CALCULATING.value == 'calculating'
        assert PeriodStatus.CLOSED.value == 'closed'

    def test_period_dataclass(self):
        """Test Period dataclass creation."""
        period = Period(
            id=1,
            period_start=date(2025, 1, 1),
            period_end=date(2025, 1, 31),
            period_type='monthly',
            status='open',
            total_costs=Decimal('100.50')
        )

        assert period.id == 1
        assert period.period_start == date(2025, 1, 1)
        assert period.period_end == date(2025, 1, 31)
        assert period.total_costs == Decimal('100.50')

    def test_create_period_success(self, period_manager, sample_period_data):
        """Test successful period creation."""
        period_id = period_manager.create_period(
            period_start=sample_period_data['period_start'],
            period_end=sample_period_data['period_end'],
            period_type=sample_period_data['period_type']
        )

        assert period_id is not None
        assert isinstance(period_id, int)

        # Verify period was created
        created_period = period_manager.get_period_by_id(period_id)
        assert created_period is not None
        assert created_period['period_start'] == sample_period_data['period_start'].isoformat()
        assert created_period['period_end'] == sample_period_data['period_end'].isoformat()
        assert created_period['status'] == PeriodStatus.OPEN.value

    def test_create_period_invalid_dates(self, period_manager):
        """Test period creation with invalid date range."""
        with pytest.raises(ValueError, match="Period end date must be after start date"):
            period_manager.create_period(
                period_start=date(2025, 1, 31),
                period_end=date(2025, 1, 1)  # End before start
            )

    def test_create_period_with_loss_carried_forward(self, period_manager, sample_period_data):
        """Test period creation with loss carried forward."""
        loss_amount = Decimal('25.50')
        period_id = period_manager.create_period(
            period_start=sample_period_data['period_start'],
            period_end=sample_period_data['period_end'],
            loss_carried_forward=loss_amount
        )

        created_period = period_manager.get_period_by_id(period_id)
        assert created_period['loss_carried_forward'] == loss_amount

    def test_find_overlapping_periods(self, period_manager, sample_period_data):
        """Test overlap detection functionality."""
        # Create first period
        period_id1 = period_manager.create_period(
            period_start=sample_period_data['period_start'],
            period_end=sample_period_data['period_end']
        )

        # Test overlapping period detection
        overlapping = period_manager.find_overlapping_periods(
            period_start=date(2025, 1, 15),  # Overlaps with existing
            period_end=date(2025, 2, 15)
        )

        assert len(overlapping) == 1
        assert overlapping[0]['id'] == period_id1

    def test_create_overlapping_period_fails(self, period_manager, sample_period_data):
        """Test that creating overlapping periods fails."""
        # Create first period
        period_manager.create_period(
            period_start=sample_period_data['period_start'],
            period_end=sample_period_data['period_end']
        )

        # Try to create overlapping period
        with pytest.raises(ValueError, match="Period overlaps with existing periods"):
            period_manager.create_period(
                period_start=date(2025, 1, 15),  # Overlaps
                period_end=date(2025, 2, 15)
            )

    def test_update_period_status_valid_transition(self, period_manager, sample_period_data):
        """Test valid period status transitions."""
        period_id = period_manager.create_period(
            period_start=sample_period_data['period_start'],
            period_end=sample_period_data['period_end']
        )

        # Transition from OPEN to CALCULATING
        success = period_manager.update_period_status(period_id, PeriodStatus.CALCULATING.value)
        assert success is True

        updated_period = period_manager.get_period_by_id(period_id)
        assert updated_period['status'] == PeriodStatus.CALCULATING.value

        # Transition from CALCULATING to CLOSED
        success = period_manager.update_period_status(period_id, PeriodStatus.CLOSED.value)
        assert success is True

        updated_period = period_manager.get_period_by_id(period_id)
        assert updated_period['status'] == PeriodStatus.CLOSED.value

    def test_update_period_status_invalid_status(self, period_manager, sample_period_data):
        """Test update with invalid status."""
        period_id = period_manager.create_period(
            period_start=sample_period_data['period_start'],
            period_end=sample_period_data['period_end']
        )

        with pytest.raises(ValueError, match="Invalid status 'invalid'"):
            period_manager.update_period_status(period_id, 'invalid')

    def test_update_period_status_nonexistent_period(self, period_manager):
        """Test update status for non-existent period."""
        with pytest.raises(ValueError, match="Period #999 not found"):
            period_manager.update_period_status(999, PeriodStatus.CALCULATING.value)

    def test_calculate_period_costs(self, period_manager, sample_period_data, temp_db):
        """Test period cost calculation functionality."""
        # Create period
        period_id = period_manager.create_period(
            period_start=sample_period_data['period_start'],
            period_end=sample_period_data['period_end']
        )

        # Set up cost manager and add test data
        finance_models = FinanceModels(temp_db)
        cost_manager = CostItemManager(temp_db)

        # Get categories
        infra_cat = cost_manager.get_category_by_name('Infrastructure')
        software_cat = cost_manager.get_category_by_name('Software')

        # Create test cost items
        monthly_item = CostItem(
            category_id=infra_cat['id'],
            name='Monthly Server',
            cost_type='monthly',
            amount_eur=Decimal('25.00'),
            starting_from_date=date(2024, 12, 1)  # Started before period
        )

        one_time_item = CostItem(
            category_id=software_cat['id'],
            name='One-time License',
            cost_type='one_time',
            amount_eur=Decimal('50.00'),
            starting_from_date=date(2025, 1, 15)  # Within period
        )

        cost_manager.create_cost_item(monthly_item)
        cost_manager.create_cost_item(one_time_item)

        # Calculate period costs
        calculation_result = period_manager.calculate_period_costs(period_id)

        # Verify calculation results
        assert calculation_result['period_id'] == period_id
        assert calculation_result['monthly_costs'] == 25.0
        assert calculation_result['one_time_costs'] == 50.0
        assert calculation_result['total_costs'] == 75.0

        # Verify period was updated
        updated_period = period_manager.get_period_by_id(period_id)
        assert updated_period['total_costs'] == Decimal('75.00')

    def test_close_period(self, period_manager, sample_period_data):
        """Test period closure functionality."""
        period_id = period_manager.create_period(
            period_start=sample_period_data['period_start'],
            period_end=sample_period_data['period_end']
        )

        # Close the period
        success = period_manager.close_period(period_id)
        assert success is True

        # Verify period is closed
        closed_period = period_manager.get_period_by_id(period_id)
        assert closed_period['status'] == PeriodStatus.CLOSED.value

    def test_close_period_already_closed(self, period_manager, sample_period_data):
        """Test closing an already closed period."""
        period_id = period_manager.create_period(
            period_start=sample_period_data['period_start'],
            period_end=sample_period_data['period_end']
        )

        # Close period first time
        period_manager.close_period(period_id)

        # Close again (should succeed without error)
        success = period_manager.close_period(period_id)
        assert success is True

    def test_close_nonexistent_period(self, period_manager):
        """Test closing non-existent period."""
        with pytest.raises(ValueError, match="Period #999 not found"):
            period_manager.close_period(999)

    def test_list_periods_no_filter(self, period_manager, sample_period_data):
        """Test listing all periods without filters."""
        # Create multiple periods
        period_id1 = period_manager.create_period(
            period_start=date(2025, 1, 1),
            period_end=date(2025, 1, 31)
        )

        period_id2 = period_manager.create_period(
            period_start=date(2025, 2, 1),
            period_end=date(2025, 2, 28)
        )

        # List all periods
        periods = period_manager.list_periods()

        assert len(periods) == 2
        period_ids = [p['id'] for p in periods]
        assert period_id1 in period_ids
        assert period_id2 in period_ids

    def test_list_periods_with_status_filter(self, period_manager):
        """Test listing periods with status filter."""
        # Create periods with different statuses
        period_id1 = period_manager.create_period(
            period_start=date(2025, 1, 1),
            period_end=date(2025, 1, 31)
        )

        period_id2 = period_manager.create_period(
            period_start=date(2025, 2, 1),
            period_end=date(2025, 2, 28)
        )

        # Close one period
        period_manager.close_period(period_id2)

        # Filter by open status
        open_periods = period_manager.list_periods(status_filter=PeriodStatus.OPEN.value)
        assert len(open_periods) == 1
        assert open_periods[0]['id'] == period_id1

        # Filter by closed status
        closed_periods = period_manager.list_periods(status_filter=PeriodStatus.CLOSED.value)
        assert len(closed_periods) == 1
        assert closed_periods[0]['id'] == period_id2

    def test_list_periods_with_date_filters(self, period_manager):
        """Test listing periods with date range filters."""
        # Create periods in different months
        jan_period = period_manager.create_period(
            period_start=date(2025, 1, 1),
            period_end=date(2025, 1, 31)
        )

        feb_period = period_manager.create_period(
            period_start=date(2025, 2, 1),
            period_end=date(2025, 2, 28)
        )

        # Filter by start date
        periods_from_feb = period_manager.list_periods(start_date=date(2025, 2, 1))
        assert len(periods_from_feb) == 1
        assert periods_from_feb[0]['id'] == feb_period

        # Filter by end date
        periods_until_jan = period_manager.list_periods(end_date=date(2025, 1, 31))
        assert len(periods_until_jan) == 1
        assert periods_until_jan[0]['id'] == jan_period

    def test_get_current_period(self, period_manager):
        """Test getting current period for a specific date."""
        # Create period covering January 2025
        period_id = period_manager.create_period(
            period_start=date(2025, 1, 1),
            period_end=date(2025, 1, 31)
        )

        # Test date within period
        current = period_manager.get_current_period(date(2025, 1, 15))
        assert current is not None
        assert current['id'] == period_id

        # Test date outside period
        current = period_manager.get_current_period(date(2025, 2, 15))
        assert current is None

    def test_get_current_period_defaults_to_today(self, period_manager):
        """Test that get_current_period defaults to today's date."""
        today = date.today()

        # Create period covering today
        period_id = period_manager.create_period(
            period_start=date(today.year, today.month, 1),
            period_end=date(today.year, today.month, 31) if today.month != 12
                   else date(today.year, 12, 31)
        )

        # Get current period without specifying date
        current = period_manager.get_current_period()
        assert current is not None
        assert current['id'] == period_id

    def test_create_monthly_period(self, period_manager):
        """Test convenience method for creating monthly periods."""
        period_id = period_manager.create_monthly_period(2025, 3)
        assert period_id is not None

        # Verify correct dates were set
        period = period_manager.get_period_by_id(period_id)
        assert period['period_start'] == '2025-03-01'
        assert period['period_end'] == '2025-03-31'
        assert period['period_type'] == 'monthly'

    def test_create_monthly_period_december(self, period_manager):
        """Test creating monthly period for December (year boundary)."""
        period_id = period_manager.create_monthly_period(2025, 12)

        period = period_manager.get_period_by_id(period_id)
        assert period['period_start'] == '2025-12-01'
        assert period['period_end'] == '2025-12-31'

    def test_auto_create_period_for_date(self, period_manager):
        """Test automatic period creation for a given date."""
        test_date = date(2025, 5, 15)

        # First call should create new period
        period_id = period_manager.auto_create_period_for_date(test_date)
        assert period_id is not None

        # Second call should return existing period
        period_id2 = period_manager.auto_create_period_for_date(test_date)
        assert period_id2 == period_id

        # Verify period covers the test date
        period = period_manager.get_period_by_id(period_id)
        assert period['period_start'] == '2025-05-01'
        assert period['period_end'] == '2025-05-31'

    def test_period_calculation_with_loss_carried_forward(self, period_manager, temp_db):
        """Test period calculation including loss carried forward."""
        # Create period with loss carried forward
        period_id = period_manager.create_period(
            period_start=date(2025, 1, 1),
            period_end=date(2025, 1, 31),
            loss_carried_forward=Decimal('15.75')
        )

        # Add a cost item
        cost_manager = CostItemManager(temp_db)
        infra_cat = cost_manager.get_category_by_name('Infrastructure')

        cost_item = CostItem(
            category_id=infra_cat['id'],
            name='Test Server',
            cost_type='monthly',
            amount_eur=Decimal('10.00'),
            starting_from_date=date(2025, 1, 1)
        )
        cost_manager.create_cost_item(cost_item)

        # Calculate costs
        calculation = period_manager.calculate_period_costs(period_id)

        # Should include loss carried forward
        assert calculation['loss_carried_forward'] == 15.75
        assert calculation['monthly_costs'] == 10.0
        assert calculation['total_costs'] == 25.75  # 10.0 + 15.75

    def test_period_cost_calculation_edge_cases(self, period_manager, temp_db):
        """Test period cost calculation with various edge cases."""
        # Create period
        period_id = period_manager.create_period(
            period_start=date(2025, 3, 1),
            period_end=date(2025, 3, 31)
        )

        cost_manager = CostItemManager(temp_db)
        infra_cat = cost_manager.get_category_by_name('Infrastructure')

        # Item that starts before period and ends during period
        item1 = CostItem(
            category_id=infra_cat['id'],
            name='Ending Item',
            cost_type='monthly',
            amount_eur=Decimal('20.00'),
            starting_from_date=date(2025, 1, 1),
            ending_date=date(2025, 3, 15)
        )

        # Item that starts after period
        item2 = CostItem(
            category_id=infra_cat['id'],
            name='Future Item',
            cost_type='monthly',
            amount_eur=Decimal('30.00'),
            starting_from_date=date(2025, 4, 1)
        )

        # One-time item outside period
        item3 = CostItem(
            category_id=infra_cat['id'],
            name='Past One-time',
            cost_type='one_time',
            amount_eur=Decimal('100.00'),
            starting_from_date=date(2025, 2, 15)
        )

        cost_manager.create_cost_item(item1)
        cost_manager.create_cost_item(item2)
        cost_manager.create_cost_item(item3)

        # Calculate costs
        calculation = period_manager.calculate_period_costs(period_id)

        # Only item1 should be included (ends during period)
        assert calculation['monthly_costs'] == 20.0
        assert calculation['one_time_costs'] == 0.0
        assert calculation['total_costs'] == 20.0

    def test_error_handling_database_errors(self, period_manager):
        """Test error handling for database-related issues."""
        # Test with invalid period ID
        with pytest.raises(ValueError, match="Period #-1 not found"):
            period_manager.calculate_period_costs(-1)

        # Test getting non-existent period
        result = period_manager.get_period_by_id(99999)
        assert result is None