"""
Tests for MarkiTect cost item management system.

This module tests the complete cost item management functionality including:
- Cost item lifecycle (create, update, deactivate)
- Category management
- Business rule validation
- Period-based cost calculations
- Integration with database models
"""

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

from markitect.finance.cost_manager import CostItemManager, CostItem, CostCategory
from markitect.finance.models import FinanceModels


class TestCostItemManager:
    """Test suite for cost item 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 cost_manager(self, temp_db):
        """Create CostItemManager instance with initialized database."""
        finance_models = FinanceModels(temp_db)
        finance_models.initialize_finance_schema()
        return CostItemManager(temp_db)

    @pytest.fixture
    def sample_category_id(self, cost_manager):
        """Create a sample category for testing."""
        return cost_manager.create_category("Test Category", "For testing purposes")

    def test_create_cost_item_valid(self, cost_manager, sample_category_id):
        """Test creating a valid cost item."""
        cost_item = CostItem(
            category_id=sample_category_id,
            name="Test Server",
            description="Monthly hosting",
            cost_type="monthly",
            amount_eur=Decimal('25.50'),
            starting_from_date=date(2025, 1, 1)
        )

        cost_item_id = cost_manager.create_cost_item(cost_item)
        assert cost_item_id is not None

        # Verify item was created
        retrieved = cost_manager.get_cost_item(cost_item_id)
        assert retrieved['name'] == "Test Server"
        assert float(retrieved['amount_eur']) == 25.50
        assert retrieved['cost_type'] == "monthly"
        assert retrieved['is_active'] == 1  # SQLite stores booleans as integers

    def test_create_cost_item_validation_errors(self, cost_manager, sample_category_id):
        """Test cost item validation errors."""
        # Missing name
        with pytest.raises(ValueError, match="name is required"):
            cost_item = CostItem(
                category_id=sample_category_id,
                name="",
                cost_type="monthly",
                amount_eur=Decimal('10.00'),
                starting_from_date=date(2025, 1, 1)
            )
            cost_manager.create_cost_item(cost_item)

        # Invalid cost type
        with pytest.raises(ValueError, match="must be 'monthly' or 'one_time'"):
            cost_item = CostItem(
                category_id=sample_category_id,
                name="Test Item",
                cost_type="invalid",
                amount_eur=Decimal('10.00'),
                starting_from_date=date(2025, 1, 1)
            )
            cost_manager.create_cost_item(cost_item)

        # Negative amount
        with pytest.raises(ValueError, match="must be non-negative"):
            cost_item = CostItem(
                category_id=sample_category_id,
                name="Test Item",
                cost_type="monthly",
                amount_eur=Decimal('-10.00'),
                starting_from_date=date(2025, 1, 1)
            )
            cost_manager.create_cost_item(cost_item)

        # Invalid date range
        with pytest.raises(ValueError, match="must be after starting date"):
            cost_item = CostItem(
                category_id=sample_category_id,
                name="Test Item",
                cost_type="monthly",
                amount_eur=Decimal('10.00'),
                starting_from_date=date(2025, 1, 15),
                ending_date=date(2025, 1, 10)
            )
            cost_manager.create_cost_item(cost_item)

        # Inactive without ending date
        with pytest.raises(ValueError, match="must have an ending date"):
            cost_item = CostItem(
                category_id=sample_category_id,
                name="Test Item",
                cost_type="monthly",
                amount_eur=Decimal('10.00'),
                starting_from_date=date(2025, 1, 1),
                is_active=False
            )
            cost_manager.create_cost_item(cost_item)

    def test_update_cost_item(self, cost_manager, sample_category_id):
        """Test updating cost item."""
        # Create initial cost item
        cost_item = CostItem(
            category_id=sample_category_id,
            name="Original Name",
            cost_type="monthly",
            amount_eur=Decimal('10.00'),
            starting_from_date=date(2025, 1, 1)
        )
        cost_item_id = cost_manager.create_cost_item(cost_item)

        # Update the cost item
        updates = {
            'name': 'Updated Name',
            'amount_eur': Decimal('15.50'),
            'description': 'Updated description'
        }
        success = cost_manager.update_cost_item(cost_item_id, updates)
        assert success is True

        # Verify updates
        updated = cost_manager.get_cost_item(cost_item_id)
        assert updated['name'] == 'Updated Name'
        assert float(updated['amount_eur']) == 15.50
        assert updated['description'] == 'Updated description'

    def test_update_nonexistent_cost_item(self, cost_manager):
        """Test updating non-existent cost item."""
        with pytest.raises(ValueError, match="not found"):
            cost_manager.update_cost_item(99999, {'name': 'New Name'})

    def test_deactivate_cost_item(self, cost_manager, sample_category_id):
        """Test deactivating cost item."""
        # Create cost item
        cost_item = CostItem(
            category_id=sample_category_id,
            name="Test Item",
            cost_type="monthly",
            amount_eur=Decimal('10.00'),
            starting_from_date=date(2025, 1, 1)
        )
        cost_item_id = cost_manager.create_cost_item(cost_item)

        # Deactivate with specific ending date
        ending_date = date(2025, 6, 30)
        success = cost_manager.deactivate_cost_item(cost_item_id, ending_date)
        assert success is True

        # Verify deactivation
        updated = cost_manager.get_cost_item(cost_item_id)
        assert updated['is_active'] == 0  # SQLite stores False as 0
        assert updated['ending_date'] == ending_date.isoformat()

    def test_list_cost_items_filtering(self, cost_manager, sample_category_id):
        """Test listing cost items with filtering."""
        # Create multiple cost items
        items = [
            CostItem(
                category_id=sample_category_id,
                name="Monthly Item 1",
                cost_type="monthly",
                amount_eur=Decimal('10.00'),
                starting_from_date=date(2025, 1, 1)
            ),
            CostItem(
                category_id=sample_category_id,
                name="One-time Item",
                cost_type="one_time",
                amount_eur=Decimal('50.00'),
                starting_from_date=date(2025, 1, 1)
            ),
            CostItem(
                category_id=sample_category_id,
                name="Inactive Item",
                cost_type="monthly",
                amount_eur=Decimal('5.00'),
                starting_from_date=date(2025, 1, 1),
                ending_date=date(2025, 1, 31),
                is_active=False
            )
        ]

        for item in items:
            cost_manager.create_cost_item(item)

        # Test filtering by active only
        active_items = cost_manager.list_cost_items(active_only=True)
        assert len(active_items) == 2
        assert all(item['is_active'] == 1 for item in active_items)

        # Test filtering by cost type
        monthly_items = cost_manager.list_cost_items(cost_type="monthly")
        assert len(monthly_items) == 1  # Only active monthly items
        assert monthly_items[0]['cost_type'] == "monthly"

        # Test including inactive items
        all_items = cost_manager.list_cost_items(active_only=False)
        assert len(all_items) == 3

    def test_get_active_costs_for_period(self, cost_manager, sample_category_id):
        """Test retrieving active costs for specific period."""
        # Create cost items with different date ranges
        items = [
            CostItem(
                category_id=sample_category_id,
                name="Active Throughout",
                cost_type="monthly",
                amount_eur=Decimal('10.00'),
                starting_from_date=date(2024, 12, 1)
            ),
            CostItem(
                category_id=sample_category_id,
                name="Starts Mid-Period",
                cost_type="monthly",
                amount_eur=Decimal('15.00'),
                starting_from_date=date(2025, 1, 15)
            ),
            CostItem(
                category_id=sample_category_id,
                name="Ends Mid-Period",
                cost_type="monthly",
                amount_eur=Decimal('20.00'),
                starting_from_date=date(2024, 12, 1),
                ending_date=date(2025, 1, 15)
            ),
            CostItem(
                category_id=sample_category_id,
                name="Outside Period",
                cost_type="monthly",
                amount_eur=Decimal('25.00'),
                starting_from_date=date(2025, 2, 1)
            )
        ]

        for item in items:
            cost_manager.create_cost_item(item)

        # Get active costs for January 2025
        period_start = date(2025, 1, 1)
        period_end = date(2025, 1, 31)
        active_costs = cost_manager.get_active_costs_for_period(period_start, period_end)

        # Should include first 3 items but not the fourth
        assert len(active_costs) == 3
        names = [item['name'] for item in active_costs]
        assert "Active Throughout" in names
        assert "Starts Mid-Period" in names
        assert "Ends Mid-Period" in names
        assert "Outside Period" not in names

    def test_calculate_period_costs(self, cost_manager, sample_category_id):
        """Test period cost calculations."""
        # Create another category
        other_category_id = cost_manager.create_category("Other Category")

        # Create cost items in different categories
        items = [
            CostItem(
                category_id=sample_category_id,
                name="Monthly Cost 1",
                cost_type="monthly",
                amount_eur=Decimal('10.00'),
                starting_from_date=date(2025, 1, 1)
            ),
            CostItem(
                category_id=sample_category_id,
                name="Monthly Cost 2",
                cost_type="monthly",
                amount_eur=Decimal('15.00'),
                starting_from_date=date(2025, 1, 1)
            ),
            CostItem(
                category_id=other_category_id,
                name="One-time Cost",
                cost_type="one_time",
                amount_eur=Decimal('100.00'),
                starting_from_date=date(2025, 1, 1)
            )
        ]

        for item in items:
            cost_manager.create_cost_item(item)

        # Calculate costs for January 2025
        period_start = date(2025, 1, 1)
        period_end = date(2025, 1, 31)
        calculations = cost_manager.calculate_period_costs(period_start, period_end)

        assert calculations['total_monthly'] == 25.00
        assert calculations['total_one_time'] == 100.00
        assert calculations['total_period'] == 125.00
        assert calculations['active_cost_items'] == 3

        # Check category breakdown
        assert 'Test Category' in calculations['category_breakdown']
        assert 'Other Category' in calculations['category_breakdown']
        assert calculations['category_breakdown']['Test Category']['monthly'] == 25.00
        assert calculations['category_breakdown']['Other Category']['one_time'] == 100.00

    def test_category_management(self, cost_manager):
        """Test category creation and management."""
        # Create category with unique name
        category_id = cost_manager.create_category("Custom Infrastructure", "Custom server costs")
        assert category_id is not None

        # Retrieve category
        category = cost_manager.get_category(category_id)
        assert category['name'] == "Custom Infrastructure"
        assert category['description'] == "Custom server costs"

        # Test duplicate category
        with pytest.raises(ValueError, match="already exists"):
            cost_manager.create_category("Custom Infrastructure")

        # List categories
        categories = cost_manager.list_categories()
        category_names = [cat['name'] for cat in categories]
        assert "Custom Infrastructure" in category_names
        # Should also include default categories from schema initialization
        assert len(categories) >= 9  # 8 default + 1 created

        # Get category by name
        found_category = cost_manager.get_category_by_name("Custom Infrastructure")
        assert found_category['id'] == category_id

    def test_cost_item_with_category_validation(self, cost_manager):
        """Test cost item creation with category validation."""
        # Try to create cost item with non-existent category
        with pytest.raises(ValueError, match="does not exist"):
            cost_item = CostItem(
                category_id=99999,
                name="Test Item",
                cost_type="monthly",
                amount_eur=Decimal('10.00'),
                starting_from_date=date(2025, 1, 1)
            )
            cost_manager.create_cost_item(cost_item)

    def test_precision_handling(self, cost_manager, sample_category_id):
        """Test decimal precision in cost calculations."""
        # Create cost item with precise decimal
        cost_item = CostItem(
            category_id=sample_category_id,
            name="Precise Cost",
            cost_type="monthly",
            amount_eur=Decimal('10.99'),
            starting_from_date=date(2025, 1, 1)
        )
        cost_item_id = cost_manager.create_cost_item(cost_item)

        # Verify precision is maintained
        retrieved = cost_manager.get_cost_item(cost_item_id)
        assert float(retrieved['amount_eur']) == 10.99

        # Test in period calculations
        calculations = cost_manager.calculate_period_costs(date(2025, 1, 1), date(2025, 1, 31))
        assert calculations['total_monthly'] == 10.99

    def test_empty_database_operations(self, cost_manager):
        """Test operations on empty database."""
        # List items in empty database
        items = cost_manager.list_cost_items()
        assert len(items) == 0

        # Get non-existent item
        item = cost_manager.get_cost_item(99999)
        assert item is None

        # Calculate costs for empty period
        calculations = cost_manager.calculate_period_costs(date(2025, 1, 1), date(2025, 1, 31))
        assert calculations['total_monthly'] == 0.00
        assert calculations['total_one_time'] == 0.00
        assert calculations['active_cost_items'] == 0