""" Tests for MarkiTect period management CLI commands. This module tests the command-line interface for period management including: - Period creation, listing, and status management - Period calculation and lifecycle operations - CLI error handling and validation """ import pytest import tempfile import os from datetime import date from decimal import Decimal from click.testing import CliRunner from markitect.finance.cli import cost_commands from markitect.finance.models import FinanceModels from markitect.finance.period_manager import PeriodManager from markitect.finance.cost_manager import CostItemManager, CostItem class TestPeriodCLICommands: """Test suite for period management CLI commands.""" @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 setup_test_data(self, temp_db): """Setup test database with sample period data.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() period_manager = PeriodManager(temp_db) # Create sample period period_id = period_manager.create_period( period_start=date(2025, 1, 1), period_end=date(2025, 1, 31), period_type='monthly' ) return temp_db, period_id @pytest.fixture def runner(self): """Create Click test runner.""" return CliRunner() def test_period_create_success(self, runner, temp_db): """Test period creation via CLI.""" # Initialize database first finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'create', '--start-date', '2025-02-01', '--end-date', '2025-02-28', '--database', temp_db ]) assert result.exit_code == 0 assert "✅ Created period #" in result.output assert "📅 Period: 2025-02-01 to 2025-02-28" in result.output assert "📊 Type: monthly" in result.output def test_period_create_with_loss_forward(self, runner, temp_db): """Test period creation with loss carried forward.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'create', '--start-date', '2025-03-01', '--end-date', '2025-03-31', '--loss-forward', '15.75', '--database', temp_db ]) assert result.exit_code == 0 assert "✅ Created period #" in result.output assert "💸 Loss carried forward: €15.7500" in result.output def test_period_create_invalid_dates(self, runner, temp_db): """Test period creation with invalid date format.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'create', '--start-date', 'invalid-date', '--end-date', '2025-02-28', '--database', temp_db ]) assert result.exit_code == 1 assert "Error: Dates must be in YYYY-MM-DD format" in result.output def test_period_create_overlapping_fails(self, runner, setup_test_data): """Test that creating overlapping periods fails.""" temp_db, existing_period_id = setup_test_data result = runner.invoke(cost_commands, [ 'period', 'create', '--start-date', '2025-01-15', # Overlaps with existing period '--end-date', '2025-02-15', '--database', temp_db ]) assert result.exit_code == 1 assert "Error:" in result.output assert "overlaps" in result.output.lower() def test_period_list_all(self, runner, setup_test_data): """Test listing all periods.""" temp_db, period_id = setup_test_data result = runner.invoke(cost_commands, [ 'period', 'list', '--database', temp_db ]) assert result.exit_code == 0 assert "📅 Calculation Periods" in result.output assert "2025-01-01" in result.output assert "2025-01-31" in result.output assert "Total: 1 periods" in result.output def test_period_list_with_status_filter(self, runner, setup_test_data): """Test listing periods with status filter.""" temp_db, period_id = setup_test_data # Create second period and close it period_manager = PeriodManager(temp_db) period_id2 = period_manager.create_period( period_start=date(2025, 2, 1), period_end=date(2025, 2, 28) ) period_manager.close_period(period_id2) # Filter by open status result = runner.invoke(cost_commands, [ 'period', 'list', '--status', 'open', '--database', temp_db ]) assert result.exit_code == 0 assert "2025-01-01" in result.output # First period should be shown assert "2025-02-01" not in result.output # Second period should be filtered out # Filter by closed status result = runner.invoke(cost_commands, [ 'period', 'list', '--status', 'closed', '--database', temp_db ]) assert result.exit_code == 0 assert "2025-02-01" in result.output # Second period should be shown assert "2025-01-01" not in result.output # First period should be filtered out def test_period_list_with_date_filters(self, runner, temp_db): """Test listing periods with date range filters.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() period_manager = PeriodManager(temp_db) # Create periods in different months jan_period = period_manager.create_period(date(2025, 1, 1), date(2025, 1, 31)) feb_period = period_manager.create_period(date(2025, 2, 1), date(2025, 2, 28)) # Filter by start date result = runner.invoke(cost_commands, [ 'period', 'list', '--start-from', '2025-02-01', '--database', temp_db ]) assert result.exit_code == 0 assert "2025-02-01" in result.output assert "2025-01-01" not in result.output def test_period_list_empty(self, runner, temp_db): """Test listing periods when none exist.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'list', '--database', temp_db ]) assert result.exit_code == 0 assert "No periods found matching criteria" in result.output def test_period_show_details(self, runner, setup_test_data): """Test showing period details.""" temp_db, period_id = setup_test_data result = runner.invoke(cost_commands, [ 'period', 'show', str(period_id), '--database', temp_db ]) assert result.exit_code == 0 assert f"📅 Period #{period_id} Details" in result.output assert "Start Date: 2025-01-01" in result.output assert "End Date: 2025-01-31" in result.output assert "Type: monthly" in result.output assert "Status: open" in result.output def test_period_show_nonexistent(self, runner, temp_db): """Test showing non-existent period.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'show', '999', '--database', temp_db ]) assert result.exit_code == 1 assert "Period #999 not found" in result.output def test_period_calculate(self, runner, setup_test_data): """Test period cost calculation.""" temp_db, period_id = setup_test_data # Add some cost items for calculation 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('25.00'), starting_from_date=date(2025, 1, 1) ) cost_manager.create_cost_item(cost_item) result = runner.invoke(cost_commands, [ 'period', 'calculate', str(period_id), '--database', temp_db ]) assert result.exit_code == 0 assert f"📊 Period #{period_id} Cost Calculation" in result.output assert "Period: 2025-01-01 to 2025-01-31" in result.output assert "Monthly Recurring: €25.00" in result.output assert "Total Period Cost: €25.00" in result.output def test_period_calculate_nonexistent(self, runner, temp_db): """Test calculating costs for non-existent period.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'calculate', '999', '--database', temp_db ]) assert result.exit_code == 1 assert "Error calculating period:" in result.output def test_period_status_update(self, runner, setup_test_data): """Test period status update.""" temp_db, period_id = setup_test_data result = runner.invoke(cost_commands, [ 'period', 'status', str(period_id), 'calculating', '--database', temp_db ]) assert result.exit_code == 0 assert f"✅ Period #{period_id} status updated to 'calculating'" in result.output # Verify the status was actually updated result = runner.invoke(cost_commands, [ 'period', 'show', str(period_id), '--database', temp_db ]) assert "Status: calculating" in result.output def test_period_status_update_invalid_status(self, runner, setup_test_data): """Test period status update with invalid status.""" temp_db, period_id = setup_test_data result = runner.invoke(cost_commands, [ 'period', 'status', str(period_id), 'invalid', '--database', temp_db ]) assert result.exit_code == 2 # Click validation error assert "Invalid value" in result.output def test_period_status_update_nonexistent(self, runner, temp_db): """Test status update for non-existent period.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'status', '999', 'calculating', '--database', temp_db ]) assert result.exit_code == 1 assert "Error:" in result.output def test_period_close(self, runner, setup_test_data): """Test period closure.""" temp_db, period_id = setup_test_data result = runner.invoke(cost_commands, [ 'period', 'close', str(period_id), '--database', temp_db ]) assert result.exit_code == 0 assert f"✅ Period #{period_id} has been closed" in result.output assert "💰 Final total cost:" in result.output # Verify the period is actually closed result = runner.invoke(cost_commands, [ 'period', 'show', str(period_id), '--database', temp_db ]) assert "Status: closed" in result.output def test_period_close_nonexistent(self, runner, temp_db): """Test closing non-existent period.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'close', '999', '--database', temp_db ]) assert result.exit_code == 1 assert "Error:" in result.output def test_period_current_exists(self, runner, setup_test_data): """Test finding current period when it exists.""" temp_db, period_id = setup_test_data result = runner.invoke(cost_commands, [ 'period', 'current', '--date', '2025-01-15', '--database', temp_db ]) assert result.exit_code == 0 assert "📅 Current Active Period" in result.output assert f"Period #{period_id}" in result.output assert "Dates: 2025-01-01 to 2025-01-31" in result.output def test_period_current_not_found(self, runner, temp_db): """Test finding current period when none exists.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'current', '--date', '2025-03-15', '--database', temp_db ]) assert result.exit_code == 0 assert "No active period found for 2025-03-15" in result.output def test_period_current_default_to_today(self, runner, temp_db): """Test current period defaults to today.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'current', '--database', temp_db ]) assert result.exit_code == 0 assert "No active period found for today" in result.output assert "💡 Create one with:" in result.output assert "markitect cost period create" in result.output def test_period_current_invalid_date(self, runner, temp_db): """Test current period with invalid date format.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'current', '--date', 'invalid-date', '--database', temp_db ]) assert result.exit_code == 1 assert "Error: Date must be in YYYY-MM-DD format" in result.output def test_period_help_commands(self, runner): """Test help output for period commands.""" # Test main period help result = runner.invoke(cost_commands, ['period', '--help']) assert result.exit_code == 0 assert "Manage calculation periods and lifecycle" in result.output # Test create help result = runner.invoke(cost_commands, ['period', 'create', '--help']) assert result.exit_code == 0 assert "Create a new calculation period" in result.output # Test list help result = runner.invoke(cost_commands, ['period', 'list', '--help']) assert result.exit_code == 0 assert "List calculation periods with optional filtering" in result.output def test_period_commands_missing_database(self, runner): """Test period commands without database specification.""" # These should use default config path and still work or show appropriate error result = runner.invoke(cost_commands, [ 'period', 'list' ]) # Should succeed with default database configuration assert result.exit_code == 0 def test_period_create_quarterly_type(self, runner, temp_db): """Test creating quarterly period type.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'period', 'create', '--start-date', '2025-04-01', '--end-date', '2025-06-30', '--type', 'quarterly', '--database', temp_db ]) assert result.exit_code == 0 assert "✅ Created period #" in result.output assert "📊 Type: quarterly" in result.output