"""
Tests for AI-powered bio generation system.

Tests cover:
- Bio generation with mocked OpenAI responses
- Caching mechanism (30-day cache)
- Bio regeneration triggers
- Error handling and fallbacks
- Batch generation with rate limiting
"""

import pytest
from datetime import datetime, timedelta
from unittest.mock import Mock, patch, MagicMock
import sys
import os

# Add parent directory to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from dating.bio_generator import (
    generate_dating_bio,
    save_bio_to_database,
    get_or_generate_bio,
    regenerate_bio_on_life_change,
    batch_generate_bios
)


# Test fixtures
@pytest.fixture
def mock_person_dict():
    """Mock person as dictionary"""
    return {
        'id': 1,
        'firstname': 'Alice',
        'ageYears': 25,
        'sex': 'Female',
        'occupation': 'Software Engineer',
        'likes': ['coding', 'hiking', 'coffee'],
        'bio': None,
        'last_bio_update': None
    }


@pytest.fixture
def mock_person_object():
    """Mock person as object"""
    class MockPerson:
        def __init__(self):
            self.id = 2
            self.firstname = 'Bob'
            self.ageYears = 28
            self.sex = 'Male'
            self.occupation = 'Teacher'
            self.likes = ['reading', 'running', 'music']
            self.bio = None
            self.last_bio_update = None

    return MockPerson()


@pytest.fixture
def mock_db_connection():
    """Mock database connection"""
    mock_conn = Mock()
    mock_cursor = Mock()

    # Configure cursor to return dictionary results
    mock_cursor.fetchone.return_value = None
    mock_conn.cursor.return_value = mock_cursor

    return mock_conn


@pytest.fixture
def mock_openai_response():
    """Mock OpenAI API response"""
    mock_response = Mock()
    mock_choice = Mock()
    mock_message = Mock()
    mock_message.content = "I'm a passionate software engineer who loves solving complex problems. When I'm not coding, you'll find me hiking local trails or discovering new coffee spots!"
    mock_choice.message = mock_message
    mock_response.choices = [mock_choice]
    return mock_response


class TestGenerateDatingBio:
    """Test bio generation functionality"""

    @patch('dating.bio_generator.get_openai_client')
    def test_generate_bio_with_dict(self, mock_get_client, mock_person_dict, mock_openai_response):
        """Test bio generation with person as dictionary"""
        mock_client = Mock()
        mock_client.chat.completions.create.return_value = mock_openai_response
        mock_get_client.return_value = mock_client

        bio = generate_dating_bio(mock_person_dict)

        assert bio is not None
        assert len(bio) > 0
        assert isinstance(bio, str)
        mock_client.chat.completions.create.assert_called_once()

    @patch('dating.bio_generator.get_openai_client')
    def test_generate_bio_with_object(self, mock_get_client, mock_person_object, mock_openai_response):
        """Test bio generation with person as object"""
        mock_client = Mock()
        mock_client.chat.completions.create.return_value = mock_openai_response
        mock_get_client.return_value = mock_client

        bio = generate_dating_bio(mock_person_object)

        assert bio is not None
        assert len(bio) > 0
        mock_client.chat.completions.create.assert_called_once()

    @patch('dating.bio_generator.get_openai_client')
    def test_generate_bio_with_minimal_data(self, mock_get_client, mock_openai_response):
        """Test bio generation with minimal person data"""
        minimal_person = {
            'firstname': 'Charlie',
            'ageYears': 22
        }
        mock_client = Mock()
        mock_client.chat.completions.create.return_value = mock_openai_response
        mock_get_client.return_value = mock_client

        bio = generate_dating_bio(minimal_person)

        assert bio is not None
        assert 'Charlie' in bio or len(bio) > 0

    @patch('dating.bio_generator.get_openai_client')
    def test_generate_bio_removes_quotes(self, mock_get_client):
        """Test that bio generation removes surrounding quotes"""
        mock_response = Mock()
        mock_choice = Mock()
        mock_message = Mock()
        mock_message.content = '"I love hiking and coffee"'  # Quoted response
        mock_choice.message = mock_message
        mock_response.choices = [mock_choice]
        mock_client = Mock()
        mock_client.chat.completions.create.return_value = mock_response
        mock_get_client.return_value = mock_client

        bio = generate_dating_bio({'firstname': 'Test', 'ageYears': 25})

        assert not bio.startswith('"')
        assert not bio.endswith('"')

    @patch('dating.bio_generator.get_openai_client')
    def test_generate_bio_api_error_fallback(self, mock_get_client, mock_person_dict):
        """Test fallback bio when OpenAI API fails"""
        mock_client = Mock()
        mock_client.chat.completions.create.side_effect = Exception("API Error")
        mock_get_client.return_value = mock_client

        bio = generate_dating_bio(mock_person_dict)

        # Should return fallback bio
        assert bio is not None
        assert 'Alice' in bio
        assert 'Software Engineer' in bio.lower() or '25' in bio

    @patch('dating.bio_generator.get_openai_client')
    def test_bio_prompt_includes_key_attributes(self, mock_get_client, mock_person_dict, mock_openai_response):
        """Test that prompt includes person's key attributes"""
        mock_client = Mock()
        mock_client.chat.completions.create.return_value = mock_openai_response
        mock_get_client.return_value = mock_client

        generate_dating_bio(mock_person_dict)

        # Get the call arguments
        call_args = mock_client.chat.completions.create.call_args

        # Check that the prompt includes key information
        messages = call_args.kwargs['messages']
        user_message = messages[1]['content']

        assert 'Alice' in user_message
        assert '25' in user_message
        assert 'Software Engineer' in user_message
        assert 'coding' in user_message or 'hiking' in user_message


class TestSaveBioToDatabase:
    """Test database save functionality"""

    def test_save_bio_executes_update_query(self, mock_db_connection):
        """Test that save_bio executes correct SQL"""
        person_id = 1
        bio = "Test bio content"
        mock_cursor = mock_db_connection.cursor.return_value

        save_bio_to_database(person_id, bio, mock_db_connection)

        # Verify cursor operations
        mock_cursor.execute.assert_called_once()
        mock_db_connection.commit.assert_called_once()
        mock_cursor.close.assert_called_once()

        # Verify SQL query
        call_args = mock_cursor.execute.call_args
        sql_query = call_args[0][0]
        params = call_args[0][1]

        assert 'UPDATE persons' in sql_query
        assert 'bio' in sql_query
        assert 'last_bio_update' in sql_query
        assert params == (bio, person_id)


class TestGetOrGenerateBio:
    """Test get or generate bio with caching"""

    @patch('dating.bio_generator.generate_dating_bio')
    @patch('dating.bio_generator.save_bio_to_database')
    def test_generates_bio_when_none_exists(self, mock_save, mock_generate, mock_db_connection):
        """Test bio generation when person has no bio"""
        person_id = 1
        mock_cursor = mock_db_connection.cursor.return_value
        mock_cursor.fetchone.return_value = {
            'id': person_id,
            'firstname': 'Alice',
            'ageYears': 25,
            'occupation': 'Engineer',
            'likes': ['coding'],
            'bio': None,
            'last_bio_update': None
        }

        mock_generate.return_value = "Generated bio"

        bio = get_or_generate_bio(person_id, mock_db_connection)

        assert bio == "Generated bio"
        mock_generate.assert_called_once()
        mock_save.assert_called_once_with(person_id, "Generated bio", mock_db_connection)

    @patch('dating.bio_generator.generate_dating_bio')
    @patch('dating.bio_generator.save_bio_to_database')
    def test_returns_cached_bio_when_recent(self, mock_save, mock_generate, mock_db_connection):
        """Test that recent bio is returned without regeneration"""
        person_id = 1
        cached_bio = "Existing bio from last week"
        recent_update = datetime.now() - timedelta(days=7)

        mock_cursor = mock_db_connection.cursor.return_value
        mock_cursor.fetchone.return_value = {
            'id': person_id,
            'firstname': 'Alice',
            'ageYears': 25,
            'occupation': 'Engineer',
            'likes': ['coding'],
            'bio': cached_bio,
            'last_bio_update': recent_update
        }

        bio = get_or_generate_bio(person_id, mock_db_connection)

        assert bio == cached_bio
        mock_generate.assert_not_called()
        mock_save.assert_not_called()

    @patch('dating.bio_generator.generate_dating_bio')
    @patch('dating.bio_generator.save_bio_to_database')
    def test_regenerates_bio_when_old(self, mock_save, mock_generate, mock_db_connection):
        """Test bio regeneration when older than 30 days"""
        person_id = 1
        old_bio = "Old bio from 2 months ago"
        old_update = datetime.now() - timedelta(days=60)
        new_bio = "Fresh new bio"

        mock_cursor = mock_db_connection.cursor.return_value
        mock_cursor.fetchone.return_value = {
            'id': person_id,
            'firstname': 'Alice',
            'ageYears': 25,
            'occupation': 'Engineer',
            'likes': ['coding'],
            'sex': 'Female',
            'bio': old_bio,
            'last_bio_update': old_update
        }

        mock_generate.return_value = new_bio

        bio = get_or_generate_bio(person_id, mock_db_connection)

        assert bio == new_bio
        mock_generate.assert_called_once()
        mock_save.assert_called_once_with(person_id, new_bio, mock_db_connection)

    @patch('dating.bio_generator.generate_dating_bio')
    @patch('dating.bio_generator.save_bio_to_database')
    def test_force_regenerate_ignores_cache(self, mock_save, mock_generate, mock_db_connection):
        """Test force_regenerate bypasses cache"""
        person_id = 1
        cached_bio = "Recent cached bio"
        recent_update = datetime.now() - timedelta(days=1)
        new_bio = "Forced new bio"

        mock_cursor = mock_db_connection.cursor.return_value
        mock_cursor.fetchone.return_value = {
            'id': person_id,
            'firstname': 'Alice',
            'ageYears': 25,
            'occupation': 'Engineer',
            'likes': ['coding'],
            'sex': 'Female',
            'bio': cached_bio,
            'last_bio_update': recent_update
        }

        mock_generate.return_value = new_bio

        bio = get_or_generate_bio(person_id, mock_db_connection, force_regenerate=True)

        assert bio == new_bio
        mock_generate.assert_called_once()
        mock_save.assert_called_once()

    def test_raises_error_for_nonexistent_person(self, mock_db_connection):
        """Test error handling for non-existent person"""
        person_id = 999
        mock_cursor = mock_db_connection.cursor.return_value
        mock_cursor.fetchone.return_value = None

        with pytest.raises(ValueError, match="Person with id 999 not found"):
            get_or_generate_bio(person_id, mock_db_connection)


class TestRegenerateBioOnLifeChange:
    """Test bio regeneration based on life events"""

    @patch('dating.bio_generator.get_or_generate_bio')
    def test_regenerates_on_job_change(self, mock_get_or_generate, mock_db_connection):
        """Test regeneration on job change event"""
        person_id = 1
        mock_get_or_generate.return_value = "New bio"

        bio = regenerate_bio_on_life_change(person_id, mock_db_connection, 'job_change')

        assert bio == "New bio"
        mock_get_or_generate.assert_called_once_with(
            person_id, mock_db_connection, force_regenerate=True
        )

    @patch('dating.bio_generator.get_or_generate_bio')
    def test_regenerates_on_graduation(self, mock_get_or_generate, mock_db_connection):
        """Test regeneration on graduation event"""
        person_id = 1
        mock_get_or_generate.return_value = "Updated bio"

        bio = regenerate_bio_on_life_change(person_id, mock_db_connection, 'graduation')

        assert bio == "Updated bio"
        mock_get_or_generate.assert_called_once()

    @patch('dating.bio_generator.get_or_generate_bio')
    def test_regenerates_on_major_life_events(self, mock_get_or_generate, mock_db_connection):
        """Test regeneration on various major life events"""
        person_id = 1
        major_events = ['promotion', 'breakup', 'marriage', 'major_achievement']

        for event in major_events:
            mock_get_or_generate.reset_mock()
            mock_get_or_generate.return_value = f"Bio after {event}"

            bio = regenerate_bio_on_life_change(person_id, mock_db_connection, event)

            assert bio is not None
            mock_get_or_generate.assert_called_once_with(
                person_id, mock_db_connection, force_regenerate=True
            )

    @patch('dating.bio_generator.get_or_generate_bio')
    def test_no_regeneration_on_minor_events(self, mock_get_or_generate, mock_db_connection):
        """Test no regeneration on minor events"""
        person_id = 1
        minor_events = ['ate_food', 'went_to_school', 'talked_to_friend']

        for event in minor_events:
            bio = regenerate_bio_on_life_change(person_id, mock_db_connection, event)

            assert bio is None
            mock_get_or_generate.assert_not_called()
            mock_get_or_generate.reset_mock()


class TestBatchGenerateBios:
    """Test batch bio generation with rate limiting"""

    @patch('dating.bio_generator.get_or_generate_bio')
    def test_batch_generate_multiple_bios(self, mock_get_or_generate, mock_db_connection):
        """Test generating bios for multiple people"""
        person_ids = [1, 2, 3]
        mock_get_or_generate.side_effect = ["Bio 1", "Bio 2", "Bio 3"]

        # Mock cursor for checking timestamps
        mock_cursor = mock_db_connection.cursor.return_value
        mock_cursor.fetchone.return_value = {
            'last_bio_update': datetime.now() - timedelta(hours=1)
        }

        results = batch_generate_bios(person_ids, mock_db_connection, max_api_calls=10)

        assert len(results) == 3
        assert results[1] == "Bio 1"
        assert results[2] == "Bio 2"
        assert results[3] == "Bio 3"

    @patch('dating.bio_generator.get_or_generate_bio')
    def test_batch_respects_max_api_calls(self, mock_get_or_generate, mock_db_connection):
        """Test rate limiting in batch generation"""
        person_ids = [1, 2, 3, 4, 5]
        mock_get_or_generate.return_value = "Generated bio"

        # Mock cursor to indicate fresh updates (API calls)
        mock_cursor = mock_db_connection.cursor.return_value
        mock_cursor.fetchone.return_value = {
            'last_bio_update': datetime.now()  # Very recent = API call was made
        }

        results = batch_generate_bios(person_ids, mock_db_connection, max_api_calls=3)

        # Should stop after 3 API calls
        assert len(results) == 3

    @patch('dating.bio_generator.get_or_generate_bio')
    def test_batch_handles_errors_gracefully(self, mock_get_or_generate, mock_db_connection):
        """Test error handling in batch generation"""
        person_ids = [1, 2, 3]

        # Make second person fail
        mock_get_or_generate.side_effect = [
            "Bio 1",
            Exception("API Error"),
            "Bio 3"
        ]

        mock_cursor = mock_db_connection.cursor.return_value
        mock_cursor.fetchone.return_value = {
            'last_bio_update': datetime.now() - timedelta(hours=1)
        }

        results = batch_generate_bios(person_ids, mock_db_connection, max_api_calls=10)

        assert len(results) == 3
        assert results[1] == "Bio 1"
        assert results[2] is None  # Error case
        assert results[3] == "Bio 3"


class TestBioContent:
    """Test bio content quality and format"""

    @patch('dating.bio_generator.get_openai_client')
    def test_bio_excludes_emojis(self, mock_get_client, mock_person_dict):
        """Test that bios don't include emojis"""
        # The prompt explicitly asks for no emojis
        call_args = None

        def capture_call(*args, **kwargs):
            nonlocal call_args
            call_args = kwargs
            mock_response = Mock()
            mock_choice = Mock()
            mock_message = Mock()
            mock_message.content = "Clean bio without emojis"
            mock_choice.message = mock_message
            mock_response.choices = [mock_choice]
            return mock_response

        mock_client = Mock()
        mock_client.chat.completions.create.side_effect = capture_call
        mock_get_client.return_value = mock_client

        generate_dating_bio(mock_person_dict)

        # Check that prompt specifies no emojis
        assert call_args is not None
        messages = call_args['messages']
        user_message = messages[1]['content']
        assert 'Do not include emojis' in user_message

    @patch('dating.bio_generator.get_openai_client')
    def test_bio_length_constraint(self, mock_get_client, mock_person_dict):
        """Test that bio prompt specifies length constraint"""
        call_args = None

        def capture_call(*args, **kwargs):
            nonlocal call_args
            call_args = kwargs
            mock_response = Mock()
            mock_choice = Mock()
            mock_message = Mock()
            mock_message.content = "Short bio"
            mock_choice.message = mock_message
            mock_response.choices = [mock_choice]
            return mock_response

        mock_client = Mock()
        mock_client.chat.completions.create.side_effect = capture_call
        mock_get_client.return_value = mock_client

        generate_dating_bio(mock_person_dict)

        # Check that prompt specifies character limit
        assert call_args is not None
        messages = call_args['messages']
        user_message = messages[1]['content']
        assert '150 characters' in user_message or 'under 150' in user_message
