""" Tests for MarkiTect cost report template generator. This module tests the complete cost report generation functionality including: - Report generation in different formats (summary, detailed, audit) - Markdown output with frontmatter and contentmatter - CLI integration and command functionality - Template structure validation """ import pytest import tempfile import os import json from datetime import date, datetime from decimal import Decimal from markitect.finance.cost_manager import CostItemManager, CostItem from markitect.finance.report_generator import CostReportGenerator, ReportConfig from markitect.finance.models import FinanceModels class TestCostReportGenerator: """Test suite for cost report generation 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 setup_test_data(self, temp_db): """Setup test database with sample cost data.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() 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 sample cost items cost_items = [ CostItem( category_id=infra_cat['id'], name='Hosteurope Server', description='Monthly server hosting', cost_type='monthly', amount_eur=Decimal('10.00'), starting_from_date=date(2025, 1, 1) ), CostItem( category_id=software_cat['id'], name='Bubble.io Plan', description='No-code platform subscription', cost_type='monthly', amount_eur=Decimal('32.00'), starting_from_date=date(2025, 1, 1) ), CostItem( category_id=infra_cat['id'], name='SSL Certificate', description='Annual SSL certificate', cost_type='one_time', amount_eur=Decimal('45.00'), starting_from_date=date(2025, 1, 15) ) ] for item in cost_items: cost_manager.create_cost_item(item) return temp_db @pytest.fixture def report_generator(self, setup_test_data): """Create report generator with test data.""" return CostReportGenerator(setup_test_data) def test_report_config_creation(self): """Test ReportConfig dataclass creation.""" config = ReportConfig( format="summary", period_start=date(2025, 1, 1), period_end=date(2025, 1, 31), currency="EUR" ) assert config.format == "summary" assert config.period_start == date(2025, 1, 1) assert config.period_end == date(2025, 1, 31) assert config.currency == "EUR" assert config.include_inactive is False assert config.output_path is None def test_generate_summary_report(self, report_generator): """Test generation of summary cost report.""" config = ReportConfig( format="summary", period_start=date(2025, 1, 1), period_end=date(2025, 1, 31) ) report = report_generator.generate_report(config) # Check that it's valid markdown with frontmatter assert report.startswith("---") assert "Cost Summary Report - January 2025" in report assert "total_costs: 87.0" in report assert "report_type: \"cost_summary\"" in report # Check contentmatter is present assert "contentmatter:" in report assert "cost_data" in report # Verify total costs are correct (10 + 32 + 45 = 87) assert "€87.00" in report def test_generate_detailed_report(self, report_generator): """Test generation of detailed cost report.""" config = ReportConfig( format="detailed", period_start=date(2025, 1, 1), period_end=date(2025, 1, 31) ) report = report_generator.generate_report(config) # Check report structure assert "Detailed Cost Report - January 2025" in report assert "Executive Summary" in report assert "report_type: \"cost_detailed\"" in report # Check category sections are present assert "Infrastructure" in report assert "Software" in report # Check individual items are listed assert "Hosteurope Server" in report assert "Bubble.io Plan" in report assert "SSL Certificate" in report # Check table format assert "| Name | Type | Amount | Status | Start Date |" in report def test_generate_audit_report(self, report_generator): """Test generation of audit trail report.""" config = ReportConfig( format="audit", period_start=date(2025, 1, 1), period_end=date(2025, 1, 31) ) report = report_generator.generate_report(config) # Check report structure assert "Cost Audit Report - January 2025" in report assert "Audit Summary" in report assert "report_type: \"cost_audit\"" in report assert "audit_trail: True" in report # Check audit sections assert "Cost Verification" in report assert "Active Cost Items" in report assert "Transaction History" in report assert "Audit Trail" in report # Check contentmatter includes audit data assert "audit_data" in report def test_generate_period_report_convenience_method(self, report_generator): """Test convenience method for generating monthly reports.""" report = report_generator.generate_period_report(2025, 1, "summary") assert "Cost Summary Report - January 2025" in report assert "2025-01-01" in report assert "2025-01-31" in report def test_invalid_report_format_raises_error(self, report_generator): """Test that invalid report format raises ValueError.""" config = ReportConfig( format="invalid", period_start=date(2025, 1, 1), period_end=date(2025, 1, 31) ) with pytest.raises(ValueError, match="Unknown report format"): report_generator.generate_report(config) def test_frontmatter_structure(self, report_generator): """Test frontmatter structure in generated reports.""" config = ReportConfig( format="summary", period_start=date(2025, 1, 1), period_end=date(2025, 1, 31) ) report = report_generator.generate_report(config) # Extract frontmatter (between first two ---) lines = report.split('\n') frontmatter_lines = [] in_frontmatter = False for line in lines: if line.strip() == "---": if not in_frontmatter: in_frontmatter = True continue else: break if in_frontmatter: frontmatter_lines.append(line) frontmatter_text = '\n'.join(frontmatter_lines) # Check required frontmatter fields assert 'report_type:' in frontmatter_text assert 'period_start:' in frontmatter_text assert 'period_end:' in frontmatter_text assert 'total_costs:' in frontmatter_text assert 'currency:' in frontmatter_text assert 'generated_at:' in frontmatter_text def test_contentmatter_structure(self, report_generator): """Test contentmatter structure in generated reports.""" config = ReportConfig( format="summary", period_start=date(2025, 1, 1), period_end=date(2025, 1, 31) ) report = report_generator.generate_report(config) # Extract contentmatter (JSON in HTML comment) assert "" in report # Find and extract JSON start = report.find("contentmatter:\n") + len("contentmatter:\n") end = report.find("\n-->") json_text = report[start:end].strip() # Parse JSON to verify structure contentmatter = json.loads(json_text) assert "cost_data" in contentmatter assert "total_monthly" in contentmatter["cost_data"] assert "total_one_time" in contentmatter["cost_data"] assert "categories" in contentmatter["cost_data"] assert "active_items" in contentmatter["cost_data"] # Verify totals assert contentmatter["cost_data"]["total_monthly"] == 42.0 assert contentmatter["cost_data"]["total_one_time"] == 45.0 def test_save_report_to_file(self, report_generator, temp_db): """Test saving report to file.""" config = ReportConfig( format="summary", period_start=date(2025, 1, 1), period_end=date(2025, 1, 31) ) report = report_generator.generate_report(config) # Save to temporary file with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.md') as f: output_path = f.name try: report_generator.save_report(report, output_path) # Verify file was created and contains expected content with open(output_path, 'r', encoding='utf-8') as f: saved_content = f.read() assert saved_content == report assert "Cost Summary Report" in saved_content finally: os.unlink(output_path) def test_empty_database_report(self, temp_db): """Test report generation with empty database.""" # Initialize empty database finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() report_generator = CostReportGenerator(temp_db) config = ReportConfig( format="summary", period_start=date(2025, 1, 1), period_end=date(2025, 1, 31) ) report = report_generator.generate_report(config) # Should still generate valid report with zero costs assert "total_costs: 0.0" in report assert "€0.00" in report def test_different_currency(self, report_generator): """Test report generation with different currency.""" config = ReportConfig( format="summary", period_start=date(2025, 1, 1), period_end=date(2025, 1, 31), currency="USD" ) report = report_generator.generate_report(config) assert 'currency: "USD"' in report # Note: amounts are still in EUR from database, currency is just metadata def test_report_with_inactive_items(self, setup_test_data): """Test report behavior with inactive cost items.""" cost_manager = CostItemManager(setup_test_data) # Deactivate one item items = cost_manager.list_cost_items() if items: cost_manager.deactivate_cost_item(items[0]['id'], date(2025, 1, 15)) report_generator = CostReportGenerator(setup_test_data) config = ReportConfig( format="detailed", period_start=date(2025, 1, 1), period_end=date(2025, 1, 31), include_inactive=False ) report = report_generator.generate_report(config) # Should still generate valid report, potentially with fewer active items assert "Detailed Cost Report" in report assert "contentmatter:" in report def test_cross_month_period(self, report_generator): """Test report generation across multiple months.""" config = ReportConfig( format="summary", period_start=date(2025, 1, 15), period_end=date(2025, 2, 15) ) report = report_generator.generate_report(config) assert "2025-01-15" in report assert "2025-02-15" in report # Should include items active during this period