"""
Tests for Issue #113 - Issue Activity Tracking Implementation

This module contains comprehensive tests for the issue activity tracking
service and CLI commands that log, retrieve, and manage issue activities
for cost allocation and project management.
"""

import pytest
import sqlite3
from datetime import datetime, date
from unittest.mock import Mock, patch, MagicMock
from pathlib import Path
import tempfile
import json
import csv
import io
from contextlib import redirect_stdout

from markitect.issues.activity_tracker import IssueActivityTracker, ActivityType, IssueActivity
from markitect.issues.activity_commands import activity


class TestActivityType:
    """Test suite for ActivityType enumeration."""

    def test_activity_type_values(self):
        """Test that all expected activity types are available."""
        expected_types = {
            "created", "modified", "closed", "reopened", "commented", "status_changed"
        }
        actual_types = {at.value for at in ActivityType}
        assert actual_types == expected_types

    def test_activity_type_enumeration(self):
        """Test that ActivityType can be constructed from string values."""
        assert ActivityType("created") == ActivityType.CREATED
        assert ActivityType("modified") == ActivityType.MODIFIED
        assert ActivityType("closed") == ActivityType.CLOSED


class TestIssueActivity:
    """Test suite for IssueActivity dataclass."""

    def test_issue_activity_creation(self):
        """Test that IssueActivity objects can be created properly."""
        activity = IssueActivity(
            id=1,
            issue_id=59,
            activity_type=ActivityType.CREATED,
            activity_date=date.today(),
            activity_details="Issue created"
        )

        assert activity.id == 1
        assert activity.issue_id == 59
        assert activity.activity_type == ActivityType.CREATED
        assert activity.activity_date == date.today()
        assert activity.activity_details == "Issue created"

    def test_issue_activity_defaults(self):
        """Test that IssueActivity has proper default values."""
        activity = IssueActivity()

        assert activity.id is None
        assert activity.issue_id is None
        assert activity.activity_type is None
        assert activity.activity_date is None
        assert activity.period_id is None
        assert activity.activity_details is None
        assert activity.created_at is None


class TestIssueActivityTracker:
    """Test suite for IssueActivityTracker service."""

    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.tracker = IssueActivityTracker(self.db_path)

    def teardown_method(self):
        """Clean up test fixtures."""
        Path(self.db_path).unlink(missing_ok=True)

    def test_tracker_initialization(self):
        """Test that tracker initializes properly with database."""
        assert self.tracker.db_path == self.db_path
        assert self.tracker.finance_models is not None

        # Verify database schema was created
        with self.tracker.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='issue_activity_log'")
            assert cursor.fetchone() is not None

    def test_log_activity_basic(self):
        """Test logging a basic activity."""
        activity_id = self.tracker.log_activity(
            issue_id=59,
            activity_type=ActivityType.CREATED,
            activity_details="Test issue created"
        )

        assert activity_id is not None

        # Verify activity was stored
        with self.tracker.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT * FROM issue_activity_log WHERE id = ?", (activity_id,))
            row = cursor.fetchone()

            assert row is not None
            assert row[1] == 59  # issue_id
            assert row[2] == "created"  # activity_type
            assert row[5] == "Test issue created"  # activity_details

    def test_log_activity_with_custom_date(self):
        """Test logging activity with custom date."""
        custom_date = date(2025, 10, 1)

        activity_id = self.tracker.log_activity(
            issue_id=59,
            activity_type=ActivityType.MODIFIED,
            activity_date=custom_date
        )

        with self.tracker.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT activity_date FROM issue_activity_log WHERE id = ?", (activity_id,))
            stored_date = cursor.fetchone()[0]

            assert stored_date == "2025-10-01"

    def test_log_activity_with_period_id(self):
        """Test logging activity with specific period ID."""
        # First create a cost period
        with self.tracker.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO cost_periods (period_start, period_end, total_costs)
                VALUES ('2025-10-01', '2025-10-31', 1000.00)
            """)
            period_id = cursor.lastrowid

        activity_id = self.tracker.log_activity(
            issue_id=59,
            activity_type=ActivityType.CREATED,
            period_id=period_id
        )

        with self.tracker.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT period_id FROM issue_activity_log WHERE id = ?", (activity_id,))
            stored_period_id = cursor.fetchone()[0]

            assert stored_period_id == period_id

    def test_get_issue_activities(self):
        """Test retrieving activities for a specific issue."""
        # Log multiple activities
        self.tracker.log_activity(59, ActivityType.CREATED, activity_details="Created")
        self.tracker.log_activity(59, ActivityType.MODIFIED, activity_details="Modified")
        self.tracker.log_activity(60, ActivityType.CREATED, activity_details="Different issue")

        activities = self.tracker.get_issue_activities(59)

        assert len(activities) == 2
        assert all(a.issue_id == 59 for a in activities)
        assert activities[0].activity_type in [ActivityType.CREATED, ActivityType.MODIFIED]

    def test_get_issue_activities_with_limit(self):
        """Test retrieving activities with limit and offset."""
        # Log multiple activities
        for i in range(5):
            self.tracker.log_activity(59, ActivityType.MODIFIED, activity_details=f"Update {i}")

        activities = self.tracker.get_issue_activities(59, limit=2, offset=1)

        assert len(activities) == 2

    def test_get_activities_by_period(self):
        """Test retrieving activities by cost period."""
        # Create a cost period
        with self.tracker.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO cost_periods (period_start, period_end, total_costs)
                VALUES ('2025-10-01', '2025-10-31', 1000.00)
            """)
            period_id = cursor.lastrowid

        # Log activities in different periods
        self.tracker.log_activity(59, ActivityType.CREATED, period_id=period_id)
        self.tracker.log_activity(60, ActivityType.MODIFIED, period_id=period_id)
        # Log activity outside the period date range
        self.tracker.log_activity(61, ActivityType.CLOSED, activity_date=date(2025, 11, 1))

        activities = self.tracker.get_activities_by_period(period_id)

        assert len(activities) == 2
        assert all(a.period_id == period_id for a in activities)

    def test_get_activities_by_period_with_type_filter(self):
        """Test retrieving activities by period with type filtering."""
        # Create period
        with self.tracker.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO cost_periods (period_start, period_end, total_costs)
                VALUES ('2025-10-01', '2025-10-31', 1000.00)
            """)
            period_id = cursor.lastrowid

        # Log various activities
        self.tracker.log_activity(59, ActivityType.CREATED, period_id=period_id)
        self.tracker.log_activity(60, ActivityType.MODIFIED, period_id=period_id)
        self.tracker.log_activity(61, ActivityType.CLOSED, period_id=period_id)

        activities = self.tracker.get_activities_by_period(
            period_id,
            activity_types=[ActivityType.CREATED, ActivityType.CLOSED]
        )

        assert len(activities) == 2
        assert all(a.activity_type in [ActivityType.CREATED, ActivityType.CLOSED] for a in activities)

    def test_get_activity_summary_basic(self):
        """Test basic activity summary generation."""
        # Log some test activities
        self.tracker.log_activity(59, ActivityType.CREATED)
        self.tracker.log_activity(59, ActivityType.MODIFIED)
        self.tracker.log_activity(60, ActivityType.CREATED)

        summary = self.tracker.get_activity_summary()

        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

    def test_get_activity_summary_with_filters(self):
        """Test activity summary with date and issue filters."""
        today = date.today()
        yesterday = date(today.year, today.month, today.day - 1) if today.day > 1 else date(today.year, today.month - 1, 28)

        # Log activities on different dates
        self.tracker.log_activity(59, ActivityType.CREATED, activity_date=yesterday)
        self.tracker.log_activity(59, ActivityType.MODIFIED, activity_date=today)
        self.tracker.log_activity(60, ActivityType.CREATED, activity_date=today)

        # Test issue filter
        summary = self.tracker.get_activity_summary(issue_id=59)
        assert summary['total_activities'] == 2
        assert summary['unique_issues'] == 1

        # Test date filter
        summary = self.tracker.get_activity_summary(start_date=today)
        assert summary['total_activities'] == 2

    def test_delete_activity(self):
        """Test deleting an activity record."""
        activity_id = self.tracker.log_activity(59, ActivityType.CREATED)

        # Verify activity exists
        activities = self.tracker.get_issue_activities(59)
        assert len(activities) == 1

        # Delete activity
        result = self.tracker.delete_activity(activity_id)
        assert result is True

        # Verify activity is gone
        activities = self.tracker.get_issue_activities(59)
        assert len(activities) == 0

    def test_delete_nonexistent_activity(self):
        """Test deleting non-existent activity returns False."""
        result = self.tracker.delete_activity(99999)
        assert result is False

    def test_bulk_log_activities(self):
        """Test logging multiple activities in one transaction."""
        activities_data = [
            {
                'issue_id': 59,
                'activity_type': 'created',
                'activity_details': 'Bulk created'
            },
            {
                'issue_id': 60,
                'activity_type': 'modified',
                'activity_details': 'Bulk modified'
            }
        ]

        activity_ids = self.tracker.bulk_log_activities(activities_data)

        assert len(activity_ids) == 2
        assert all(isinstance(aid, int) for aid in activity_ids)

        # Verify activities were created
        activities_59 = self.tracker.get_issue_activities(59)
        activities_60 = self.tracker.get_issue_activities(60)

        assert len(activities_59) == 1
        assert len(activities_60) == 1
        assert activities_59[0].activity_details == 'Bulk created'
        assert activities_60[0].activity_details == 'Bulk modified'

    def test_bulk_log_activities_validation(self):
        """Test bulk logging validates required fields."""
        invalid_data = [
            {'issue_id': 59},  # Missing activity_type
            {'activity_type': 'created'}  # Missing issue_id
        ]

        with pytest.raises(ValueError, match="must have 'issue_id' and 'activity_type'"):
            self.tracker.bulk_log_activities(invalid_data)


class TestActivityCommands:
    """Test suite for activity CLI commands."""

    def setup_method(self):
        """Set up test fixtures."""
        self.temp_db = tempfile.NamedTemporaryFile(suffix='.db', delete=False)
        self.temp_db.close()
        self.db_path = self.temp_db.name

        # Initialize database with test data
        tracker = IssueActivityTracker(self.db_path)
        tracker.log_activity(59, ActivityType.CREATED, activity_details="Test issue created")
        tracker.log_activity(59, ActivityType.MODIFIED, activity_details="Test issue modified")

    def teardown_method(self):
        """Clean up test fixtures."""
        Path(self.db_path).unlink(missing_ok=True)

    @patch('markitect.issues.activity_commands.IssueActivityTracker')
    def test_log_command_basic(self, mock_tracker_class):
        """Test the log command with basic parameters."""
        mock_tracker = Mock()
        mock_tracker_class.return_value = mock_tracker
        mock_tracker.log_activity.return_value = 123

        from click.testing import CliRunner
        runner = CliRunner()

        result = runner.invoke(activity, ['log', '59', 'created'])

        assert result.exit_code == 0
        assert "✅ Logged created activity for issue #59" in result.output
        mock_tracker.log_activity.assert_called_once()

    @patch('markitect.issues.activity_commands.IssueActivityTracker')
    def test_log_command_with_details(self, mock_tracker_class):
        """Test the log command with activity details."""
        mock_tracker = Mock()
        mock_tracker_class.return_value = mock_tracker
        mock_tracker.log_activity.return_value = 123

        from click.testing import CliRunner
        runner = CliRunner()

        result = runner.invoke(activity, ['log', '59', 'created', '--details', 'Test details'])

        assert result.exit_code == 0
        assert "Test details" in result.output

    @patch('markitect.issues.activity_commands.IssueActivityTracker')
    def test_show_command(self, mock_tracker_class):
        """Test the show command for displaying issue activities."""
        mock_tracker = Mock()
        mock_tracker_class.return_value = mock_tracker

        mock_activities = [
            IssueActivity(
                id=1,
                issue_id=59,
                activity_type=ActivityType.CREATED,
                activity_date=date.today(),
                activity_details="Test activity",
                created_at=datetime.now()
            )
        ]
        mock_tracker.get_issue_activities.return_value = mock_activities

        from click.testing import CliRunner
        runner = CliRunner()

        result = runner.invoke(activity, ['show', '59'])

        assert result.exit_code == 0
        assert "📋 Activities for Issue #59" in result.output
        assert "Test activity" in result.output

    @patch('markitect.issues.activity_commands.IssueActivityTracker')
    def test_show_command_json_format(self, mock_tracker_class):
        """Test the show command with JSON output format."""
        mock_tracker = Mock()
        mock_tracker_class.return_value = mock_tracker

        mock_activities = [
            IssueActivity(
                id=1,
                issue_id=59,
                activity_type=ActivityType.CREATED,
                activity_date=date.today(),
                activity_details="Test activity",
                created_at=datetime.now()
            )
        ]
        mock_tracker.get_issue_activities.return_value = mock_activities

        from click.testing import CliRunner
        runner = CliRunner()

        result = runner.invoke(activity, ['show', '59', '--format', 'json'])

        assert result.exit_code == 0
        # Should be valid JSON
        output_data = json.loads(result.output.strip())
        assert len(output_data) == 1
        assert output_data[0]['issue_id'] == 59

    @patch('markitect.issues.activity_commands.IssueActivityTracker')
    def test_summary_command(self, mock_tracker_class):
        """Test the summary command."""
        mock_tracker = Mock()
        mock_tracker_class.return_value = mock_tracker

        mock_summary = {
            'total_activities': 5,
            'unique_issues': 3,
            'activities_by_type': {'created': 3, 'modified': 2},
            'date_range': {'start': '2025-10-01', 'end': '2025-10-04'},
            'filters': {'issue_id': None, 'start_date': None, 'end_date': None}
        }
        mock_tracker.get_activity_summary.return_value = mock_summary

        from click.testing import CliRunner
        runner = CliRunner()

        result = runner.invoke(activity, ['summary'])

        assert result.exit_code == 0
        assert "📊 Issue Activity Summary" in result.output
        assert "Total Activities: 5" in result.output
        assert "Unique Issues: 3" in result.output

    @patch('markitect.issues.activity_commands.IssueActivityTracker')
    def test_delete_command(self, mock_tracker_class):
        """Test the delete command."""
        mock_tracker = Mock()
        mock_tracker_class.return_value = mock_tracker
        mock_tracker.delete_activity.return_value = True

        from click.testing import CliRunner
        runner = CliRunner()

        # Auto-confirm the deletion
        result = runner.invoke(activity, ['delete', '123'], input='y\n')

        assert result.exit_code == 0
        assert "✅ Deleted activity #123" in result.output
        mock_tracker.delete_activity.assert_called_once_with(123)

    def test_import_activities_json(self):
        """Test importing activities from JSON file."""
        # Create test JSON file
        test_data = [
            {
                'issue_id': 59,
                'activity_type': 'created',
                'activity_details': 'Imported activity'
            }
        ]

        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
            json.dump(test_data, f)
            json_file_path = f.name

        try:
            with patch('markitect.issues.activity_commands.IssueActivityTracker') as mock_tracker_class:
                mock_tracker = Mock()
                mock_tracker_class.return_value = mock_tracker
                mock_tracker.bulk_log_activities.return_value = [1]

                from click.testing import CliRunner
                runner = CliRunner()

                result = runner.invoke(activity, ['import-activities', json_file_path])

                assert result.exit_code == 0
                assert "✅ Successfully imported 1 activities" in result.output
        finally:
            Path(json_file_path).unlink(missing_ok=True)


class TestActivityIntegration:
    """Integration tests for the complete activity tracking 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_full_activity_lifecycle(self):
        """Test the complete lifecycle of activity tracking."""
        tracker = IssueActivityTracker(self.db_path)

        # 1. Log initial activity
        activity_id = tracker.log_activity(
            issue_id=59,
            activity_type=ActivityType.CREATED,
            activity_details="Issue created for testing"
        )
        assert activity_id is not None

        # 2. Log follow-up activities (with slight time differences to ensure ordering)
        import time
        time.sleep(0.1)
        tracker.log_activity(59, ActivityType.MODIFIED, activity_details="Updated description")
        time.sleep(0.1)
        tracker.log_activity(59, ActivityType.COMMENTED, activity_details="Added comment")
        time.sleep(0.1)
        tracker.log_activity(59, ActivityType.CLOSED, activity_details="Resolved issue")

        # 3. Retrieve issue history
        activities = tracker.get_issue_activities(59)
        assert len(activities) == 4

        # Verify all expected activity types are present
        activity_types = [a.activity_type.value for a in activities]
        expected_types = {'closed', 'commented', 'modified', 'created'}
        assert set(activity_types) == expected_types

        # 4. Generate summary
        summary = tracker.get_activity_summary(issue_id=59)
        assert summary['total_activities'] == 4
        assert summary['unique_issues'] == 1
        assert len(summary['activities_by_type']) == 4

        # 5. Clean up - delete an activity
        deleted = tracker.delete_activity(activity_id)
        assert deleted is True

        # Verify deletion
        remaining_activities = tracker.get_issue_activities(59)
        assert len(remaining_activities) == 3

    def test_multi_issue_activity_tracking(self):
        """Test activity tracking across multiple issues."""
        tracker = IssueActivityTracker(self.db_path)

        # Log activities for multiple issues
        issues = [59, 60, 61, 62]
        for issue_id in issues:
            tracker.log_activity(issue_id, ActivityType.CREATED)
            if issue_id % 2 == 0:  # Even issues get modified
                tracker.log_activity(issue_id, ActivityType.MODIFIED)

        # Test overall summary
        summary = tracker.get_activity_summary()
        assert summary['total_activities'] == 6  # 4 created + 2 modified
        assert summary['unique_issues'] == 4
        assert summary['activities_by_type']['created'] == 4
        assert summary['activities_by_type']['modified'] == 2

        # Test individual issue tracking
        for issue_id in issues:
            activities = tracker.get_issue_activities(issue_id)
            expected_count = 2 if issue_id % 2 == 0 else 1
            assert len(activities) == expected_count

    def test_cost_period_integration(self):
        """Test integration with cost period functionality."""
        tracker = IssueActivityTracker(self.db_path)

        # Create cost periods
        with tracker.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO cost_periods (period_start, period_end, total_costs)
                VALUES ('2025-01-01', '2025-03-31', 5000.00)
            """)
            q1_period_id = cursor.lastrowid

            cursor.execute("""
                INSERT INTO cost_periods (period_start, period_end, total_costs)
                VALUES ('2025-04-01', '2025-06-30', 6000.00)
            """)
            q2_period_id = cursor.lastrowid

            periods = {'Q1 2025': q1_period_id, 'Q2 2025': q2_period_id}

        # Log activities in different periods
        tracker.log_activity(59, ActivityType.CREATED, period_id=periods['Q1 2025'])
        tracker.log_activity(60, ActivityType.MODIFIED, period_id=periods['Q1 2025'])
        tracker.log_activity(61, ActivityType.CLOSED, period_id=periods['Q2 2025'])

        # Test period-based retrieval
        q1_activities = tracker.get_activities_by_period(periods['Q1 2025'])
        q2_activities = tracker.get_activities_by_period(periods['Q2 2025'])

        assert len(q1_activities) == 2
        assert len(q2_activities) == 1
        assert all(a.period_id == periods['Q1 2025'] for a in q1_activities)
        assert all(a.period_id == periods['Q2 2025'] for a in q2_activities)

        # Test filtering by activity type within period
        q1_created = tracker.get_activities_by_period(
            periods['Q1 2025'],
            activity_types=[ActivityType.CREATED]
        )
        assert len(q1_created) == 1
        assert q1_created[0].activity_type == ActivityType.CREATED