""" Tests for Issue #124 - Single command Day-Wrap-Up This module contains comprehensive tests for the day wrap-up functionality that consolidates daily work summaries, activity tracking, cost distribution, and reporting into a single convenient command. """ import pytest import tempfile from datetime import datetime, date, timedelta from unittest.mock import Mock, patch, MagicMock from pathlib import Path from decimal import Decimal import json from markitect.finance.day_wrapup_commands import DayWrapUpService, wrapup, _display_daily_summary, _display_period_summary class TestDayWrapUpService: """Test suite for DayWrapUpService.""" def setup_method(self): """Set up test fixtures with temporary database.""" self.temp_db = tempfile.NamedTemporaryFile(suffix='.db', delete=False) self.temp_db.close() self.db_path = self.temp_db.name self.service = DayWrapUpService(self.db_path) def teardown_method(self): """Clean up test fixtures.""" Path(self.db_path).unlink(missing_ok=True) def test_service_initialization(self): """Test that service initializes properly with all trackers.""" assert self.service.db_path == self.db_path assert self.service.worktime_tracker is not None assert self.service.activity_tracker is not None assert self.service.session_tracker is not None def test_get_worktime_summary_no_data(self): """Test worktime summary when no data exists.""" today = date.today() summary = self.service._get_worktime_summary(today) assert summary['total_minutes'] == 0 assert summary['total_hours'] == 0.0 assert summary['issues_worked'] == 0 assert summary['entries'] == [] assert summary['cost_allocated'] is None assert summary['cost_per_minute'] is None def test_get_worktime_summary_with_data(self): """Test worktime summary with logged data.""" today = date.today() # Log some worktime self.service.worktime_tracker.log_worktime(124, 90, work_date=today, description="Main work") self.service.worktime_tracker.log_worktime(125, 60, work_date=today, description="Side work") summary = self.service._get_worktime_summary(today) assert summary['total_minutes'] == 150 # 90 + 60 assert summary['total_hours'] == 2.5 assert summary['issues_worked'] == 2 assert summary['entries'] == 2 assert len(summary['issue_breakdown']) == 2 assert 124 in summary['issue_breakdown'] assert 125 in summary['issue_breakdown'] assert summary['issue_breakdown'][124]['minutes'] == 90 assert summary['issue_breakdown'][125]['minutes'] == 60 def test_get_activity_summary_no_data(self): """Test activity summary when no data exists.""" today = date.today() summary = self.service._get_activity_summary(today) assert summary['total_activities'] == 0 assert summary['unique_issues'] == 0 assert summary['activities_by_type'] == {} assert summary['activities'] == [] def test_get_activity_summary_with_data(self): """Test activity summary with logged data.""" today = date.today() # Log some activities from markitect.issues.activity_tracker import ActivityType self.service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today, activity_details="Created issue") self.service.activity_tracker.log_activity(124, ActivityType.MODIFIED, activity_date=today, activity_details="Updated issue") self.service.activity_tracker.log_activity(125, ActivityType.CREATED, activity_date=today, activity_details="Created another") summary = self.service._get_activity_summary(today) assert summary['total_activities'] == 3 assert summary['unique_issues'] == 2 assert 'created' in summary['activities_by_type'] assert 'modified' in summary['activities_by_type'] assert summary['activities_by_type']['created'] == 2 assert summary['activities_by_type']['modified'] == 1 assert len(summary['activities']) == 3 def test_get_cost_summary_no_distribution(self): """Test cost summary when no cost distribution exists.""" today = date.today() summary = self.service._get_cost_summary(today) assert summary['daily_total'] == 0.0 assert summary['issue_costs'] == {} assert summary['has_cost_allocation'] is False def test_get_cost_summary_with_distribution(self): """Test cost summary with cost distribution data.""" today = date.today() # Log worktime and distribute costs self.service.worktime_tracker.log_worktime(124, 120, work_date=today) # 2 hours self.service.worktime_tracker.log_worktime(125, 60, work_date=today) # 1 hour distribution = self.service.worktime_tracker.distribute_daily_costs( work_date=today, total_daily_cost=Decimal('90.00') # €90 total ) summary = self.service._get_cost_summary(today) assert summary['daily_total'] == 90.0 assert summary['has_cost_allocation'] is True assert len(summary['issue_costs']) == 2 assert summary['issue_costs'][124] == 60.0 # 2/3 of €90 assert summary['issue_costs'][125] == 30.0 # 1/3 of €90 def test_generate_recommendations_no_data(self): """Test recommendation generation with no data.""" summary = { 'worktime': {'total_minutes': 0, 'total_hours': 0.0, 'issues_worked': 0}, 'activities': {'total_activities': 0, 'unique_issues': 0}, 'costs': {'has_cost_allocation': False} } recommendations = self.service._generate_recommendations(summary) assert len(recommendations) >= 2 assert any("No worktime logged" in rec for rec in recommendations) assert any("No issue activities logged" in rec for rec in recommendations) def test_generate_recommendations_with_data(self): """Test recommendation generation with various data conditions.""" # Test low worktime summary = { 'worktime': {'total_minutes': 120, 'total_hours': 2.0, 'issues_worked': 1}, 'activities': {'total_activities': 5, 'unique_issues': 1}, 'costs': {'has_cost_allocation': False} } recommendations = self.service._generate_recommendations(summary) assert any("Low worktime logged" in rec for rec in recommendations) assert any("no costs distributed" in rec for rec in recommendations) # Test high worktime summary['worktime'] = {'total_minutes': 660, 'total_hours': 11.0, 'issues_worked': 1} recommendations = self.service._generate_recommendations(summary) assert any("High worktime logged" in rec for rec in recommendations) # Test many issues summary['activities'] = {'total_activities': 10, 'unique_issues': 6} recommendations = self.service._generate_recommendations(summary) assert any("Many issues worked on" in rec for rec in recommendations) def test_perform_auto_estimation_no_activities(self): """Test auto estimation when no activities exist.""" today = date.today() result = self.service.perform_auto_estimation(today, 8.0) assert result['estimated'] is False assert "No active issues found" in result['reason'] assert result['active_issues'] == [] def test_perform_auto_estimation_with_existing_time(self): """Test auto estimation when time is already logged.""" today = date.today() # Log some worktime first self.service.worktime_tracker.log_worktime(124, 60, work_date=today) result = self.service.perform_auto_estimation(today, 8.0) assert result['estimated'] is False assert "Time already logged" in result['reason'] assert result['existing_minutes'] == 60 def test_perform_auto_estimation_success(self): """Test successful auto estimation.""" today = date.today() # Create activities for issues from markitect.issues.activity_tracker import ActivityType self.service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today) self.service.activity_tracker.log_activity(125, ActivityType.MODIFIED, activity_date=today) result = self.service.perform_auto_estimation(today, 6.0) assert result['estimated'] is True assert 'estimation_result' in result estimation = result['estimation_result'] assert estimation['total_minutes'] == 360 # 6 hours assert estimation['issues_count'] == 2 assert len(estimation['issue_estimates']) == 2 # Verify worktime entries were created entries = self.service.worktime_tracker.get_worktime_entries(work_date=today) assert len(entries) == 2 assert all(e.entry_type == "estimated" for e in entries) def test_distribute_daily_costs(self): """Test daily cost distribution functionality.""" today = date.today() # Log worktime first self.service.worktime_tracker.log_worktime(124, 180, work_date=today) # 3 hours self.service.worktime_tracker.log_worktime(125, 120, work_date=today) # 2 hours # Total: 5 hours (300 minutes) result = self.service.distribute_daily_costs(today, Decimal('150.00')) assert result['total_cost'] == 150.0 assert result['total_minutes'] == 300 assert result['cost_per_minute'] == 0.5 assert result['distributions'][124]['cost_allocated'] == 90.0 # 3/5 * €150 assert result['distributions'][125]['cost_allocated'] == 60.0 # 2/5 * €150 def test_generate_daily_summary_integration(self): """Test complete daily summary generation.""" today = date.today() # Create comprehensive test data from markitect.issues.activity_tracker import ActivityType # Log worktime self.service.worktime_tracker.log_worktime(124, 120, work_date=today, description="Main feature") self.service.worktime_tracker.log_worktime(125, 60, work_date=today, description="Bug fix") # Log activities self.service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today) self.service.activity_tracker.log_activity(124, ActivityType.MODIFIED, activity_date=today) self.service.activity_tracker.log_activity(125, ActivityType.CLOSED, activity_date=today) # Distribute costs self.service.distribute_daily_costs(today, Decimal('90.00')) # Generate summary summary = self.service.generate_daily_summary(today) # Verify summary structure assert summary['date'] == today assert 'worktime' in summary assert 'activities' in summary assert 'costs' in summary assert 'recommendations' in summary # Verify worktime data worktime = summary['worktime'] assert worktime['total_minutes'] == 180 assert worktime['total_hours'] == 3.0 assert worktime['issues_worked'] == 2 assert worktime['cost_allocated'] == 90.0 # Verify activity data activities = summary['activities'] assert activities['total_activities'] == 3 assert activities['unique_issues'] == 2 # Verify cost data costs = summary['costs'] assert costs['daily_total'] == 90.0 assert costs['has_cost_allocation'] is True # Verify recommendations exist assert isinstance(summary['recommendations'], list) class TestDayWrapUpCommands: """Test suite for day wrap-up CLI commands.""" @patch('markitect.finance.day_wrapup_commands.DayWrapUpService') def test_daily_command_basic(self, mock_service_class): """Test the daily wrap-up command with basic functionality.""" mock_service = Mock() mock_service_class.return_value = mock_service # Mock the summary data mock_summary = { 'date': date.today(), 'worktime': { 'total_minutes': 180, 'total_hours': 3.0, 'issues_worked': 2, 'entries': 2, 'issue_breakdown': {124: {'minutes': 120, 'entries': 1}, 125: {'minutes': 60, 'entries': 1}}, 'cost_allocated': 90.0, 'cost_per_minute': 0.5 }, 'activities': { 'total_activities': 3, 'unique_issues': 2, 'activities_by_type': {'created': 2, 'modified': 1}, 'activities': [] }, 'costs': { 'daily_total': 90.0, 'issue_costs': {124: 60.0, 125: 30.0}, 'has_cost_allocation': True }, 'recommendations': ["💰 Costs distributed successfully"] } mock_service.generate_daily_summary.return_value = mock_summary from click.testing import CliRunner runner = CliRunner() result = runner.invoke(wrapup, ['daily']) assert result.exit_code == 0 assert "📊 Daily Wrap-Up" in result.output assert "⏰ WORKTIME SUMMARY" in result.output assert "📝 ACTIVITIES SUMMARY" in result.output assert "💰 COST SUMMARY" in result.output assert "💡 RECOMMENDATIONS" in result.output @patch('markitect.finance.day_wrapup_commands.DayWrapUpService') def test_daily_command_with_auto_estimate(self, mock_service_class): """Test daily command with auto-estimation enabled.""" mock_service = Mock() mock_service_class.return_value = mock_service # Mock estimation result mock_estimation = { 'estimated': True, 'estimation_result': { 'total_minutes': 480, 'issues_count': 2, 'issue_estimates': {124: 240, 125: 240} } } mock_service.perform_auto_estimation.return_value = mock_estimation # Mock summary mock_service.generate_daily_summary.return_value = { 'date': date.today(), 'worktime': {'total_minutes': 0, 'total_hours': 0.0, 'issues_worked': 0, 'entries': []}, 'activities': {'total_activities': 0, 'unique_issues': 0, 'activities_by_type': {}, 'activities': []}, 'costs': {'daily_total': 0.0, 'issue_costs': {}, 'has_cost_allocation': False}, 'recommendations': [] } from click.testing import CliRunner runner = CliRunner() result = runner.invoke(wrapup, ['daily', '--auto-estimate', '--estimate-hours', '8']) assert result.exit_code == 0 assert "🤖 Auto-estimating worktime" in result.output assert "✅ Estimated 8.0h across 2 issues" in result.output mock_service.perform_auto_estimation.assert_called_once_with(date.today(), 8.0) @patch('markitect.finance.day_wrapup_commands.DayWrapUpService') def test_daily_command_with_cost_distribution(self, mock_service_class): """Test daily command with cost distribution.""" mock_service = Mock() mock_service_class.return_value = mock_service # Mock distribution result mock_distribution = { 'total_cost': 120.0, 'total_minutes': 240, 'issues_count': 2, 'distributions': {124: {'cost_allocated': 80.0}, 125: {'cost_allocated': 40.0}} } mock_service.distribute_daily_costs.return_value = mock_distribution # Mock summary mock_service.generate_daily_summary.return_value = { 'date': date.today(), 'worktime': {'total_minutes': 0, 'total_hours': 0.0, 'issues_worked': 0, 'entries': []}, 'activities': {'total_activities': 0, 'unique_issues': 0, 'activities_by_type': {}, 'activities': []}, 'costs': {'daily_total': 0.0, 'issue_costs': {}, 'has_cost_allocation': False}, 'recommendations': [] } from click.testing import CliRunner runner = CliRunner() result = runner.invoke(wrapup, ['daily', '--distribute-cost', '120']) assert result.exit_code == 0 assert "💰 Distributing €120.00" in result.output assert "✅ Distributed €120.00 across 2 issues" in result.output mock_service.distribute_daily_costs.assert_called_once() @patch('markitect.finance.day_wrapup_commands.DayWrapUpService') def test_daily_command_json_format(self, mock_service_class): """Test daily command with JSON output format.""" mock_service = Mock() mock_service_class.return_value = mock_service mock_summary = { 'date': date.today(), 'worktime': {'total_minutes': 120, 'total_hours': 2.0, 'issues_worked': 1, 'entries': 1}, 'activities': {'total_activities': 2, 'unique_issues': 1, 'activities_by_type': {}, 'activities': []}, 'costs': {'daily_total': 0.0, 'issue_costs': {}, 'has_cost_allocation': False}, 'recommendations': [] } mock_service.generate_daily_summary.return_value = mock_summary from click.testing import CliRunner runner = CliRunner() result = runner.invoke(wrapup, ['daily', '--format', 'json']) assert result.exit_code == 0 # Should be valid JSON output_data = json.loads(result.output.strip()) assert 'date' in output_data assert 'worktime' in output_data assert 'activities' in output_data assert 'costs' in output_data @patch('markitect.finance.day_wrapup_commands.DayWrapUpService') def test_estimate_command(self, mock_service_class): """Test the estimate command.""" mock_service = Mock() mock_service_class.return_value = mock_service mock_estimation = { 'estimated': True, 'estimation_result': { 'work_date': date.today(), 'total_minutes': 480, # 8 hours 'distribution_method': 'activity_based', 'issue_estimates': {124: 300, 125: 180}, # 5h and 3h 'issues_count': 2 } } mock_service.perform_auto_estimation.return_value = mock_estimation from click.testing import CliRunner runner = CliRunner() today = date.today().strftime('%Y-%m-%d') result = runner.invoke(wrapup, ['estimate', today, '--hours', '8']) assert result.exit_code == 0 assert "✅ Estimated worktime" in result.output assert "Total Hours: 8.0h" in result.output assert "Issues: 2" in result.output assert "Estimated Time Distribution:" in result.output @patch('markitect.finance.day_wrapup_commands.DayWrapUpService') def test_period_command(self, mock_service_class): """Test the period wrap-up command.""" mock_service = Mock() mock_service_class.return_value = mock_service # Mock worktime report mock_worktime_report = { 'period': '2025-10-01 to 2025-10-04', 'total_entries': 8, 'total_time': {'hours': 20, 'minutes': 30, 'total_minutes': 1230}, 'unique_issues': 3, 'unique_dates': 4, 'average_minutes_per_day': 307.5 } mock_service.worktime_tracker.get_worktime_report.return_value = mock_worktime_report # Mock activity summary mock_activity_summary = { 'total_activities': 15, 'unique_issues': 4, 'activities_by_type': {'created': 8, 'modified': 5, 'closed': 2} } mock_service.activity_tracker.get_activity_summary.return_value = mock_activity_summary from click.testing import CliRunner runner = CliRunner() result = runner.invoke(wrapup, ['period', '2025-10-01', '2025-10-04']) assert result.exit_code == 0 assert "📈 Period Wrap-Up" in result.output assert "⏰ WORKTIME OVERVIEW" in result.output assert "📝 ACTIVITIES OVERVIEW" in result.output assert "Total Time: 20h 30m" in result.output assert "Total Activities: 15" in result.output class TestDayWrapUpIntegration: """Integration tests for the complete day wrap-up system.""" def setup_method(self): """Set up integration test fixtures.""" self.temp_db = tempfile.NamedTemporaryFile(suffix='.db', delete=False) self.temp_db.close() self.db_path = self.temp_db.name def teardown_method(self): """Clean up integration test fixtures.""" Path(self.db_path).unlink(missing_ok=True) def test_complete_day_workflow(self): """Test a complete daily workflow from start to finish.""" service = DayWrapUpService(self.db_path) today = date.today() # 1. Start with empty day initial_summary = service.generate_daily_summary(today) assert initial_summary['worktime']['total_minutes'] == 0 assert initial_summary['activities']['total_activities'] == 0 # 2. Log some activities from markitect.issues.activity_tracker import ActivityType service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today, activity_details="Started new feature") service.activity_tracker.log_activity(124, ActivityType.MODIFIED, activity_date=today, activity_details="Made progress") service.activity_tracker.log_activity(125, ActivityType.CLOSED, activity_date=today, activity_details="Fixed bug") # 3. Perform auto-estimation estimation = service.perform_auto_estimation(today, 7.5) assert estimation['estimated'] is True assert estimation['estimation_result']['total_minutes'] == 450 # 7.5 hours # 4. Distribute costs distribution = service.distribute_daily_costs(today, Decimal('112.50')) # €15 per hour assert distribution['total_cost'] == 112.5 assert distribution['cost_per_minute'] == 0.25 # €0.25 per minute # 5. Generate final summary final_summary = service.generate_daily_summary(today) # Verify complete summary assert final_summary['worktime']['total_hours'] == 7.5 assert final_summary['worktime']['issues_worked'] == 2 assert final_summary['worktime']['cost_allocated'] == 112.5 assert final_summary['activities']['total_activities'] == 3 assert final_summary['activities']['unique_issues'] == 2 assert final_summary['costs']['daily_total'] == 112.5 assert final_summary['costs']['has_cost_allocation'] is True assert len(final_summary['costs']['issue_costs']) == 2 # Verify recommendations are helpful recommendations = final_summary['recommendations'] assert len(recommendations) >= 0 # Should have reasonable recommendations def test_multi_day_period_summary(self): """Test period summary across multiple days.""" service = DayWrapUpService(self.db_path) # Create data across multiple days dates = [date.today() - timedelta(days=i) for i in range(3)] # Last 3 days for i, test_date in enumerate(dates): # Log different amounts of work each day hours = 6 + i * 2 # 6, 8, 10 hours minutes = hours * 60 service.worktime_tracker.log_worktime(124, minutes // 2, work_date=test_date, description=f"Day {i+1} main work") service.worktime_tracker.log_worktime(125 + i, minutes // 2, work_date=test_date, description=f"Day {i+1} side work") # Log activities from markitect.issues.activity_tracker import ActivityType service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=test_date) service.activity_tracker.log_activity(125 + i, ActivityType.MODIFIED, activity_date=test_date) # Generate period report start_date = dates[-1] # Oldest date end_date = dates[0] # Most recent date worktime_report = service.worktime_tracker.get_worktime_report( start_date=start_date, end_date=end_date ) # Verify period data assert worktime_report['total_entries'] == 6 # 2 entries per day * 3 days assert worktime_report['total_time']['total_minutes'] == 1440 # 6+8+10 = 24 hours assert worktime_report['unique_issues'] == 4 # Issues 124, 125, 126, 127 assert worktime_report['unique_dates'] == 3 # Verify daily averages expected_avg = 1440 / 3 # 480 minutes per day on average assert abs(worktime_report['average_minutes_per_day'] - expected_avg) < 1 def test_error_handling_and_edge_cases(self): """Test error handling and edge cases.""" service = DayWrapUpService(self.db_path) today = date.today() # Test estimation with no activities estimation = service.perform_auto_estimation(today, 8.0) assert estimation['estimated'] is False assert "No active issues found" in estimation['reason'] # Test cost distribution with no worktime distribution = service.distribute_daily_costs(today, Decimal('100.00')) assert 'message' in distribution assert "No worktime entries found" in distribution['message'] # Test summary generation with partial data from markitect.issues.activity_tracker import ActivityType service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today) summary = service.generate_daily_summary(today) assert summary['worktime']['total_minutes'] == 0 # No worktime logged assert summary['activities']['total_activities'] == 1 # But activity exists assert "No worktime logged" in ' '.join(summary['recommendations']) # Test recommendations for edge cases service.worktime_tracker.log_worktime(124, 720, work_date=today) # 12 hours - excessive summary = service.generate_daily_summary(today) assert any("High worktime logged" in rec for rec in summary['recommendations'])