"""Tests for Site Management API (core/site_api.py)."""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from core.site_api import (
    MAX_SITES_PER_USER,
    create_user_site,
    delete_user_site,
    get_credential_fields,
    get_user_sites,
    validate_credentials,
    validate_site_connection,
)

# ── Fixtures ──────────────────────────────────────────────────


@pytest.fixture
def mock_db():
    """Create and patch a mock database instance."""
    db = AsyncMock()
    db.count_sites_by_user = AsyncMock(return_value=0)
    db.get_site_by_alias = AsyncMock(return_value=None)
    db.get_site = AsyncMock(
        return_value={
            "id": "site-uuid-001",
            "user_id": "user-uuid-001",
            "plugin_type": "wordpress",
            "alias": "myblog",
            "url": "https://myblog.example.com",
            "credentials": b"encrypted-blob",
            "status": "active",
            "status_msg": "Connection verified",
            "created_at": "2026-02-19T12:00:00Z",
        }
    )
    db.get_sites_by_user = AsyncMock(
        return_value=[
            {
                "id": "site-uuid-001",
                "user_id": "user-uuid-001",
                "plugin_type": "wordpress",
                "alias": "myblog",
                "url": "https://myblog.example.com",
                "credentials": b"encrypted-blob",
                "status": "active",
                "created_at": "2026-02-19T12:00:00Z",
            },
        ]
    )
    db.delete_site = AsyncMock(return_value=True)
    db.execute = AsyncMock()
    db.update_site_status = AsyncMock()
    with patch("core.database.get_database", return_value=db):
        yield db


@pytest.fixture
def mock_encryption():
    """Create and patch a mock encryption instance."""
    enc = MagicMock()
    enc.encrypt_credentials = MagicMock(return_value=b"encrypted-blob")
    enc.decrypt_credentials = MagicMock(
        return_value={
            "username": "admin",
            "app_password": "xxxx xxxx xxxx xxxx",
        }
    )
    with patch("core.encryption.get_credential_encryption", return_value=enc):
        yield enc


# ── Credential Field Definitions ─────────────────────────────


class TestCredentialFields:
    """Test credential field definitions and retrieval."""

    @pytest.mark.unit
    def test_get_credential_fields_all_plugins(self):
        """Every registered plugin type returns non-empty credential field lists."""
        expected_plugins = [
            "wordpress",
            "woocommerce",
            "wordpress_specialist",
            "gitea",
            "n8n",
            "supabase",
            "openpanel",
            "appwrite",
            "directus",
            "coolify",
        ]
        for plugin_type in expected_plugins:
            fields = get_credential_fields(plugin_type)
            assert len(fields) > 0, f"{plugin_type} returned empty fields"
            for field in fields:
                assert "name" in field
                assert "label" in field
                assert "type" in field
                assert "required" in field

    @pytest.mark.unit
    def test_get_credential_fields_invalid(self):
        """Unknown plugin type should raise ValueError."""
        with pytest.raises(ValueError, match="Unknown plugin type"):
            get_credential_fields("nonexistent_plugin")


# ── Credential Validation ────────────────────────────────────


class TestCredentialValidation:
    """Test credential validation logic."""

    @pytest.mark.unit
    def test_validate_credentials_valid(self):
        """Valid WordPress credentials should pass validation."""
        valid, errors = validate_credentials(
            "wordpress",
            {
                "username": "admin",
                "app_password": "xxxx xxxx xxxx xxxx",
            },
        )
        assert valid is True
        assert errors == []

    @pytest.mark.unit
    def test_validate_credentials_missing_required(self):
        """Missing required fields should return errors."""
        valid, errors = validate_credentials(
            "wordpress",
            {
                "username": "admin",
                # app_password is missing
            },
        )
        assert valid is False
        assert len(errors) == 1
        assert "Application Password" in errors[0]

    @pytest.mark.unit
    def test_validate_credentials_optional_field(self):
        """Optional advanced fields should not cause errors when missing."""
        valid, errors = validate_credentials(
            "wordpress",
            {
                "username": "admin",
                "app_password": "xxxx xxxx xxxx xxxx",
                # wp_app_password is optional — should not cause error
            },
        )
        assert valid is True
        assert errors == []

    @pytest.mark.unit
    def test_validate_credentials_empty_string_required(self):
        """Empty string for required field should fail validation."""
        valid, errors = validate_credentials(
            "wordpress",
            {
                "username": "admin",
                "app_password": "   ",  # whitespace only
            },
        )
        assert valid is False
        assert len(errors) == 1


# ── Site Creation ────────────────────────────────────────────


class TestSiteCreation:
    """Test site creation flow."""

    @pytest.mark.unit
    async def test_create_site_success(self, mock_db, mock_encryption):
        """Creating a site with skip_validation should succeed."""
        result = await create_user_site(
            user_id="user-uuid-001",
            plugin_type="wordpress",
            alias="myblog",
            url="https://myblog.example.com",
            credentials={"username": "admin", "app_password": "xxxx xxxx xxxx xxxx"},
            skip_validation=True,
        )
        assert result["alias"] == "myblog"
        assert result["plugin_type"] == "wordpress"
        # Credentials blob should be stripped from the response
        assert "credentials" not in result
        # DB execute should have been called to insert
        mock_db.execute.assert_called_once()
        # Encryption should have been used
        mock_encryption.encrypt_credentials.assert_called_once()

    @pytest.mark.unit
    async def test_create_site_alias_too_short(self, mock_db, mock_encryption):
        """Alias shorter than 2 characters should raise ValueError."""
        with pytest.raises(ValueError, match="2-50 characters"):
            await create_user_site(
                user_id="user-uuid-001",
                plugin_type="wordpress",
                alias="a",
                url="https://example.com",
                credentials={"username": "admin", "app_password": "xxxx"},
                skip_validation=True,
            )

    @pytest.mark.unit
    async def test_create_site_alias_invalid_chars(self, mock_db, mock_encryption):
        """Alias with invalid characters should raise ValueError."""
        with pytest.raises(ValueError, match="letters, numbers, hyphens, and underscores"):
            await create_user_site(
                user_id="user-uuid-001",
                plugin_type="wordpress",
                alias="my blog!",
                url="https://example.com",
                credentials={"username": "admin", "app_password": "xxxx"},
                skip_validation=True,
            )

    @pytest.mark.unit
    async def test_create_site_max_limit(self, mock_db, mock_encryption):
        """Exceeding MAX_SITES_PER_USER should raise ValueError."""
        mock_db.count_sites_by_user.return_value = MAX_SITES_PER_USER
        with pytest.raises(ValueError, match="Site limit reached"):
            await create_user_site(
                user_id="user-uuid-001",
                plugin_type="wordpress",
                alias="toomany",
                url="https://example.com",
                credentials={"username": "admin", "app_password": "xxxx"},
                skip_validation=True,
            )

    @pytest.mark.unit
    async def test_create_site_duplicate_alias(self, mock_db, mock_encryption):
        """Duplicate alias for the same user should raise ValueError."""
        mock_db.get_site_by_alias.return_value = {"id": "existing-site", "alias": "myblog"}
        with pytest.raises(ValueError, match="already in use"):
            await create_user_site(
                user_id="user-uuid-001",
                plugin_type="wordpress",
                alias="myblog",
                url="https://example.com",
                credentials={"username": "admin", "app_password": "xxxx"},
                skip_validation=True,
            )


# ── Site Retrieval and Deletion ──────────────────────────────


class TestSiteRetrievalDeletion:
    """Test site list and delete operations."""

    @pytest.mark.unit
    async def test_get_user_sites(self, mock_db):
        """get_user_sites should return sites without credential blobs."""
        sites = await get_user_sites("user-uuid-001")
        assert len(sites) == 1
        assert sites[0]["alias"] == "myblog"
        # Credentials should be stripped
        assert "credentials" not in sites[0]

    @pytest.mark.unit
    async def test_delete_user_site(self, mock_db):
        """delete_user_site should return True when site exists."""
        deleted = await delete_user_site("site-uuid-001", "user-uuid-001")
        assert deleted is True
        mock_db.delete_site.assert_called_once_with("site-uuid-001", "user-uuid-001")

    @pytest.mark.unit
    async def test_delete_user_site_not_found(self, mock_db):
        """delete_user_site should return False when site doesn't exist."""
        mock_db.delete_site.return_value = False
        deleted = await delete_user_site("nonexistent", "user-uuid-001")
        assert deleted is False


# ── Connection Validation ────────────────────────────────────


class TestConnectionValidation:
    """Test live connection validation with mocked HTTP."""

    @pytest.mark.unit
    async def test_validate_connection_success(self):
        """Successful HTTP response should return (True, 'OK')."""
        mock_response = AsyncMock()
        mock_response.status = 200
        mock_response.text = AsyncMock(return_value="OK")
        mock_response.__aenter__ = AsyncMock(return_value=mock_response)
        mock_response.__aexit__ = AsyncMock(return_value=False)

        mock_session = AsyncMock()
        mock_session.get = MagicMock(return_value=mock_response)
        mock_session.__aenter__ = AsyncMock(return_value=mock_session)
        mock_session.__aexit__ = AsyncMock(return_value=False)

        with patch("core.site_api.aiohttp.ClientSession", return_value=mock_session):
            ok, msg = await validate_site_connection(
                "wordpress",
                "https://myblog.example.com",
                {"username": "admin", "app_password": "xxxx"},
            )

        assert ok is True
        assert msg == "OK"

    @pytest.mark.unit
    async def test_validate_connection_timeout(self):
        """Timeout should return (False, message about timeout)."""
        mock_session = AsyncMock()
        mock_session.get = MagicMock(side_effect=TimeoutError("timed out"))
        mock_session.__aenter__ = AsyncMock(return_value=mock_session)
        mock_session.__aexit__ = AsyncMock(return_value=False)

        with patch("core.site_api.aiohttp.ClientSession", return_value=mock_session):
            ok, msg = await validate_site_connection(
                "wordpress",
                "https://slow-site.example.com",
                {"username": "admin", "app_password": "xxxx"},
            )

        assert ok is False
        assert "timed out" in msg.lower()

    @pytest.mark.unit
    async def test_validate_connection_auth_failure(self):
        """HTTP 401 should return (False, message about authentication)."""
        mock_response = AsyncMock()
        mock_response.status = 401
        mock_response.text = AsyncMock(return_value="Unauthorized")
        mock_response.__aenter__ = AsyncMock(return_value=mock_response)
        mock_response.__aexit__ = AsyncMock(return_value=False)

        mock_session = AsyncMock()
        mock_session.get = MagicMock(return_value=mock_response)
        mock_session.__aenter__ = AsyncMock(return_value=mock_session)
        mock_session.__aexit__ = AsyncMock(return_value=False)

        with patch("core.site_api.aiohttp.ClientSession", return_value=mock_session):
            ok, msg = await validate_site_connection(
                "wordpress",
                "https://myblog.example.com",
                {"username": "admin", "app_password": "wrong"},
            )

        assert ok is False
        assert "authentication" in msg.lower()
