"""
Tests for author profile discovery from IndieAuth identity

Per v1.2.0 Phase 2 and developer Q&A Q31-Q35:
- Mock HTTP requests for author discovery
- Test discovery, caching, and fallback behavior
- Ensure login never blocks on discovery failure
"""

import json
from datetime import datetime, timedelta
from unittest.mock import Mock, patch

import pytest

from starpunk.author_discovery import (
    discover_author_profile,
    get_author_profile,
    save_author_profile,
    DiscoveryError,
)


# Sample h-card HTML for testing (per Q35)
SAMPLE_HCARD_HTML = """
<!DOCTYPE html>
<html>
<head>
    <title>Alice's Profile</title>
</head>
<body class="h-card">
    <h1 class="p-name">Alice Example</h1>
    <img class="u-photo" src="https://example.com/photo.jpg" alt="Alice">
    <a class="u-url" href="https://alice.example.com">alice.example.com</a>
    <p class="p-note">IndieWeb enthusiast and developer</p>
    <a rel="me" href="https://github.com/alice">GitHub</a>
    <a rel="me" href="https://twitter.com/alice">Twitter</a>
</body>
</html>
"""

MINIMAL_HCARD_HTML = """
<!DOCTYPE html>
<html>
<head><title>Bob's Site</title></head>
<body>
    <div class="h-card">
        <a class="p-name u-url" href="https://bob.example.com">Bob</a>
    </div>
</body>
</html>
"""

NO_HCARD_HTML = """
<!DOCTYPE html>
<html>
<head><title>No Microformats Here</title></head>
<body>
    <h1>Just a regular page</h1>
</body>
</html>
"""


class TestDiscoverAuthorProfile:
    """Test author profile discovery from h-card"""

    @patch('starpunk.author_discovery.httpx.get')
    def test_discover_hcard_from_valid_profile(self, mock_get, app):
        """Discover h-card from valid profile URL"""
        # Mock HTTP response
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.text = SAMPLE_HCARD_HTML
        mock_response.raise_for_status = Mock()
        mock_get.return_value = mock_response

        with app.app_context():
            profile = discover_author_profile('https://alice.example.com')

        assert profile is not None
        assert profile['name'] == 'Alice Example'
        assert profile['photo'] == 'https://example.com/photo.jpg'
        assert profile['url'] == 'https://alice.example.com'
        assert profile['note'] == 'IndieWeb enthusiast and developer'
        assert 'https://github.com/alice' in profile['rel_me_links']
        assert 'https://twitter.com/alice' in profile['rel_me_links']

    @patch('starpunk.author_discovery.httpx.get')
    def test_discover_minimal_hcard(self, mock_get, app):
        """Handle minimal h-card with only name and URL"""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.text = MINIMAL_HCARD_HTML
        mock_response.raise_for_status = Mock()
        mock_get.return_value = mock_response

        with app.app_context():
            profile = discover_author_profile('https://bob.example.com')

        assert profile is not None
        assert profile['name'] == 'Bob'
        assert profile['url'] == 'https://bob.example.com'
        assert profile['photo'] is None
        assert profile['note'] is None
        assert profile['rel_me_links'] == []

    @patch('starpunk.author_discovery.httpx.get')
    def test_discover_no_hcard_returns_none(self, mock_get, app):
        """Gracefully handle missing h-card (per Q14)"""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.text = NO_HCARD_HTML
        mock_response.raise_for_status = Mock()
        mock_get.return_value = mock_response

        with app.app_context():
            profile = discover_author_profile('https://example.com')

        assert profile is None

    @patch('starpunk.author_discovery.httpx.get')
    def test_discover_timeout_raises_error(self, mock_get, app):
        """Handle network timeout gracefully (per Q38)"""
        import httpx
        mock_get.side_effect = httpx.TimeoutException('Timeout')

        with app.app_context():
            with pytest.raises(DiscoveryError, match='Timeout'):
                discover_author_profile('https://slow.example.com')

    @patch('starpunk.author_discovery.httpx.get')
    def test_discover_http_error_raises_error(self, mock_get, app):
        """Handle HTTP errors gracefully"""
        import httpx
        mock_response = Mock()
        mock_response.status_code = 404
        mock_get.return_value = mock_response
        mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
            'Not Found', request=Mock(), response=mock_response
        )

        with app.app_context():
            with pytest.raises(DiscoveryError, match='HTTP error'):
                discover_author_profile('https://missing.example.com')


class TestGetAuthorProfile:
    """Test author profile retrieval with caching"""

    def test_get_profile_with_cache(self, app):
        """Use cached profile if valid (per Q14, Q19)"""
        with app.app_context():
            # Save a cached profile
            test_profile = {
                'name': 'Test User',
                'photo': 'https://example.com/photo.jpg',
                'url': 'https://test.example.com',
                'note': 'Test bio',
                'rel_me_links': ['https://github.com/test'],
            }
            save_author_profile('https://test.example.com', test_profile)

            # Retrieve should use cache (no HTTP call)
            profile = get_author_profile('https://test.example.com')

            assert profile['name'] == 'Test User'
            assert profile['photo'] == 'https://example.com/photo.jpg'
            assert profile['me'] == 'https://test.example.com'

    @patch('starpunk.author_discovery.discover_author_profile')
    def test_get_profile_refresh_forces_discovery(self, mock_discover, app):
        """Force refresh bypasses cache (per Q20)"""
        mock_discover.return_value = {
            'name': 'Fresh Data',
            'photo': None,
            'url': 'https://test.example.com',
            'note': None,
            'rel_me_links': [],
        }

        with app.app_context():
            # Save old cache
            old_profile = {
                'name': 'Old Data',
                'photo': None,
                'url': 'https://test.example.com',
                'note': None,
                'rel_me_links': [],
            }
            save_author_profile('https://test.example.com', old_profile)

            # Get with refresh=True
            profile = get_author_profile('https://test.example.com', refresh=True)

            assert profile['name'] == 'Fresh Data'
            mock_discover.assert_called_once()

    def test_get_profile_expired_cache_fallback(self, app):
        """Use expired cache if discovery fails (per Q14)"""
        with app.app_context():
            # Save expired cache manually
            from starpunk.database import get_db
            db = get_db(app)

            expired_time = (datetime.utcnow() - timedelta(hours=48)).isoformat()

            db.execute(
                """
                INSERT INTO author_profile
                (me, name, photo, url, note, rel_me_links, discovered_at, cached_until)
                VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)
                """,
                (
                    'https://expired.example.com',
                    'Expired User',
                    None,
                    'https://expired.example.com',
                    None,
                    json.dumps([]),
                    expired_time,
                )
            )
            db.commit()

            # Mock discovery failure
            with patch('starpunk.author_discovery.discover_author_profile') as mock_discover:
                mock_discover.side_effect = DiscoveryError('Network error')

                # Should use expired cache as fallback
                profile = get_author_profile('https://expired.example.com')

                assert profile['name'] == 'Expired User'
                assert profile['me'] == 'https://expired.example.com'

    @patch('starpunk.author_discovery.discover_author_profile')
    def test_get_profile_no_cache_no_discovery_uses_defaults(self, mock_discover, app):
        """Use minimal defaults if no cache and discovery fails (per Q14, Q21)"""
        mock_discover.side_effect = DiscoveryError('Failed')

        with app.app_context():
            profile = get_author_profile('https://fallback.example.com')

            # Should return defaults based on URL
            assert profile['me'] == 'https://fallback.example.com'
            assert profile['name'] == 'fallback.example.com'  # domain as fallback
            assert profile['photo'] is None
            assert profile['url'] == 'https://fallback.example.com'
            assert profile['note'] is None
            assert profile['rel_me_links'] == []


class TestSaveAuthorProfile:
    """Test saving author profile to database"""

    def test_save_profile_creates_record(self, app):
        """Save profile creates database record"""
        with app.app_context():
            from starpunk.database import get_db

            profile = {
                'name': 'Save Test',
                'photo': 'https://example.com/photo.jpg',
                'url': 'https://save.example.com',
                'note': 'Test note',
                'rel_me_links': ['https://github.com/test'],
            }

            save_author_profile('https://save.example.com', profile)

            # Verify in database
            db = get_db(app)
            row = db.execute(
                "SELECT * FROM author_profile WHERE me = ?",
                ('https://save.example.com',)
            ).fetchone()

            assert row is not None
            assert row['name'] == 'Save Test'
            assert row['photo'] == 'https://example.com/photo.jpg'

            # Check rel_me_links is stored as JSON
            rel_me = json.loads(row['rel_me_links'])
            assert 'https://github.com/test' in rel_me

    def test_save_profile_sets_24_hour_cache(self, app):
        """Cache TTL is 24 hours (per Q14)"""
        with app.app_context():
            from starpunk.database import get_db

            profile = {
                'name': 'Cache Test',
                'photo': None,
                'url': 'https://cache.example.com',
                'note': None,
                'rel_me_links': [],
            }

            before_save = datetime.utcnow()
            save_author_profile('https://cache.example.com', profile)
            after_save = datetime.utcnow()

            # Check cache expiry
            db = get_db(app)
            row = db.execute(
                "SELECT cached_until FROM author_profile WHERE me = ?",
                ('https://cache.example.com',)
            ).fetchone()

            cached_until = datetime.fromisoformat(row['cached_until'])

            # Should be approximately 24 hours from now
            expected_min = before_save + timedelta(hours=23, minutes=59)
            expected_max = after_save + timedelta(hours=24, minutes=1)

            assert expected_min <= cached_until <= expected_max

    def test_save_profile_upserts_existing(self, app):
        """Saving again updates existing record"""
        with app.app_context():
            from starpunk.database import get_db

            # Save first version
            profile1 = {
                'name': 'Version 1',
                'photo': None,
                'url': 'https://upsert.example.com',
                'note': None,
                'rel_me_links': [],
            }
            save_author_profile('https://upsert.example.com', profile1)

            # Save updated version
            profile2 = {
                'name': 'Version 2',
                'photo': 'https://example.com/new.jpg',
                'url': 'https://upsert.example.com',
                'note': 'Updated bio',
                'rel_me_links': ['https://mastodon.social/@test'],
            }
            save_author_profile('https://upsert.example.com', profile2)

            # Should have only one record with updated data
            db = get_db(app)
            rows = db.execute(
                "SELECT * FROM author_profile WHERE me = ?",
                ('https://upsert.example.com',)
            ).fetchall()

            assert len(rows) == 1
            assert rows[0]['name'] == 'Version 2'
            assert rows[0]['photo'] == 'https://example.com/new.jpg'
            assert rows[0]['note'] == 'Updated bio'
