"""Tests for app/views/settings.py module."""

import base64
import json
from unittest.mock import MagicMock, Mock, patch

import pytest
from itsdangerous import TimestampSigner

from app.views.settings import require_admin_access

TEST_SESSION_SECRET = "test_secret_key_for_testing_must_be_at_least_32_characters_long"


def _make_admin_session_cookie() -> str:
    """Create a properly signed session cookie with admin user for tests."""
    session_data = {"user": {"id": "admin", "is_admin": True}}
    signer = TimestampSigner(TEST_SESSION_SECRET)
    data = base64.b64encode(json.dumps(session_data).encode()).decode("utf-8")
    return signer.sign(data).decode("utf-8")


@pytest.mark.unit
class TestRequireAdminAccess:
    """Tests for require_admin_access decorator."""

    @pytest.mark.asyncio
    async def test_redirects_non_admin_user(self):
        """Test that non-admin users are redirected."""

        @require_admin_access
        async def dummy_route(request):
            return {"success": True}

        mock_request = MagicMock()
        mock_request.session = {"user": {"id": "1", "is_admin": False}}

        result = await dummy_route(mock_request)
        assert result.status_code == 302

    @pytest.mark.asyncio
    async def test_redirects_when_no_user(self):
        """Test that unauthenticated users are redirected."""

        @require_admin_access
        async def dummy_route(request):
            return {"success": True}

        mock_request = MagicMock()
        mock_request.session = {}

        result = await dummy_route(mock_request)
        assert result.status_code == 302

    @pytest.mark.asyncio
    async def test_allows_admin_user(self):
        """Test that admin users can access the route."""

        @require_admin_access
        async def dummy_route(request):
            return {"success": True}

        mock_request = MagicMock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}

        result = await dummy_route(mock_request)
        assert result == {"success": True}

    @pytest.mark.asyncio
    async def test_redirects_to_home_page(self):
        """Test that non-admin users are redirected to home page."""

        @require_admin_access
        async def dummy_route(request):
            return {"success": True}

        mock_request = MagicMock()
        mock_request.session = {}

        result = await dummy_route(mock_request)
        assert result.status_code == 302
        assert result.headers["location"] == "/"

    @pytest.mark.asyncio
    async def test_works_with_sync_functions(self):
        """Test decorator works with synchronous functions."""

        @require_admin_access
        def sync_route(request):
            return {"success": True}

        mock_request = MagicMock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}

        result = await sync_route(mock_request)
        assert result == {"success": True}


@pytest.mark.integration
class TestSettingsView:
    """Tests for settings page endpoint."""

    def test_settings_page_redirects_without_admin(self, client):
        """Test settings page redirects non-admin users."""
        response = client.get("/settings", follow_redirects=False)
        # Should redirect since no user in session
        assert response.status_code in (200, 302, 303)

    @patch("app.views.settings.get_all_settings_from_db")
    @patch("app.views.settings.get_settings_by_category")
    @patch("app.views.settings.templates.TemplateResponse")
    def test_settings_page_returns_template(self, mock_template, mock_categories, mock_db_settings, client):
        """Test settings page returns template response."""
        mock_db_settings.return_value = {}
        mock_categories.return_value = {"General": ["workdir"]}

        # Without admin session, will redirect
        response = client.get("/settings", follow_redirects=False)
        assert response.status_code in (200, 302, 303)


@pytest.mark.unit
class TestSettingsPageLogic:
    """Tests for settings page logic."""

    @patch("app.views.settings.get_all_settings_from_db")
    @patch("app.views.settings.get_settings_by_category")
    @patch("app.views.settings.get_setting_metadata")
    @patch("app.views.settings.mask_sensitive_value")
    @patch("app.views.settings.templates")
    @patch("app.views.settings.settings")
    @patch("app.views.settings.os.environ", {"TEST_VAR": "test_value"})
    @pytest.mark.asyncio
    async def test_determines_setting_source_database(
        self, mock_settings, mock_templates, mock_mask, mock_metadata, mock_categories, mock_db_settings
    ):
        """Test determines setting source as database."""
        from app.views.settings import settings_page

        mock_db_settings.return_value = {"test_key": "db_value"}
        mock_categories.return_value = {"General": ["test_key"]}
        mock_metadata.return_value = {"type": "str", "sensitive": False}
        mock_settings.test_key = "db_value"
        mock_settings.version = "1.0.0"
        mock_mask.return_value = "db_value"

        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        # Call the function with mocked db
        await settings_page(mock_request, mock_db)

        # Verify template was called
        mock_templates.TemplateResponse.assert_called_once()

    @patch("app.views.settings.get_all_settings_from_db")
    @patch("app.views.settings.get_settings_by_category")
    @patch("app.views.settings.get_setting_metadata")
    @patch("app.views.settings.mask_sensitive_value")
    @patch("app.views.settings.os.environ", {"WORKDIR": "/tmp"})
    def test_determines_setting_source_environment(self, mock_mask, mock_metadata, mock_categories, mock_db_settings):
        """Test determines setting source as environment variable."""
        mock_db_settings.return_value = {}
        mock_categories.return_value = {"General": ["workdir"]}
        mock_metadata.return_value = {"type": "str", "sensitive": False}
        mock_mask.return_value = "/tmp"

        # The actual test would verify source determination logic

    @patch("app.views.settings.get_all_settings_from_db")
    @patch("app.views.settings.get_settings_by_category")
    @patch("app.views.settings.get_setting_metadata")
    @patch("app.views.settings.mask_sensitive_value")
    def test_determines_setting_source_default(self, mock_mask, mock_metadata, mock_categories, mock_db_settings):
        """Test determines setting source as default value."""
        mock_db_settings.return_value = {}
        mock_categories.return_value = {"General": ["workdir"]}
        mock_metadata.return_value = {"type": "str", "sensitive": False}
        mock_mask.return_value = "/app/workdir"

        # The actual test would verify source determination logic

    @patch("app.views.settings.get_setting_metadata")
    def test_masks_sensitive_values(self, mock_metadata):
        """Test masks sensitive values."""
        from app.views.settings import mask_sensitive_value

        mock_metadata.return_value = {"sensitive": True}

        # Test that sensitive values are masked
        value = "sensitive_password_123"
        masked = mask_sensitive_value(value)
        assert masked != value

    @patch("app.views.settings.get_all_settings_from_db")
    @pytest.mark.asyncio
    async def test_handles_database_errors(self, mock_db_settings):
        """Test handles database errors gracefully."""
        from app.views.settings import settings_page

        mock_db_settings.side_effect = Exception("Database error")

        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        # Should raise HTTPException
        from fastapi import HTTPException

        with pytest.raises(HTTPException) as exc_info:
            await settings_page(mock_request, mock_db)
        assert exc_info.value.status_code == 500

    @patch("app.views.settings.get_all_settings_from_db")
    @patch("app.views.settings.get_settings_by_category")
    @patch("app.views.settings.get_setting_metadata")
    @patch("app.views.settings.mask_sensitive_value")
    @patch("app.views.settings.templates")
    @patch("app.views.settings.settings")
    @patch("app.views.settings.os.environ", {"WORKDIR": "/tmp"})
    @pytest.mark.asyncio
    async def test_env_source_when_key_in_environ(
        self, mock_settings, mock_templates, mock_mask, mock_metadata, mock_categories, mock_db_settings
    ):
        """Test that env source is used when key is in os.environ."""
        from app.views.settings import settings_page

        mock_db_settings.return_value = {}
        mock_categories.return_value = {"General": ["workdir"]}
        mock_metadata.return_value = {"type": "str", "sensitive": False}
        mock_settings.workdir = "/tmp"
        mock_settings.version = "1.0.0"

        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        await settings_page(mock_request, mock_db)
        mock_templates.TemplateResponse.assert_called_once()
        call_args = mock_templates.TemplateResponse.call_args
        context = call_args[0][1]
        settings_data = context["settings_data"]
        workdir_entry = settings_data["General"][0]
        assert workdir_entry["source"] == "environment"

    @patch("app.views.settings.get_all_settings_from_db")
    @patch("app.views.settings.get_settings_by_category")
    @patch("app.views.settings.get_setting_metadata")
    @patch("app.views.settings.templates")
    @patch("app.views.settings.settings")
    @patch("app.views.settings.os.environ", {})
    @pytest.mark.asyncio
    async def test_default_source_when_key_not_in_environ(
        self, mock_settings, mock_templates, mock_metadata, mock_categories, mock_db_settings
    ):
        """Test that default source is used when key is not in environ or DB."""
        from app.views.settings import settings_page

        mock_db_settings.return_value = {}
        mock_categories.return_value = {"General": ["workdir"]}
        mock_metadata.return_value = {"type": "str", "sensitive": False}
        mock_settings.workdir = "/app/workdir"
        mock_settings.version = "1.0.0"

        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        await settings_page(mock_request, mock_db)
        call_args = mock_templates.TemplateResponse.call_args
        context = call_args[0][1]
        settings_data = context["settings_data"]
        workdir_entry = settings_data["General"][0]
        assert workdir_entry["source"] == "default"

    @patch("app.views.settings.get_all_settings_from_db")
    @patch("app.views.settings.get_settings_by_category")
    @patch("app.views.settings.get_setting_metadata")
    @patch("app.views.settings.mask_sensitive_value")
    @patch("app.views.settings.templates")
    @patch("app.views.settings.settings")
    @pytest.mark.asyncio
    async def test_sensitive_values_masked_in_settings_page(
        self, mock_settings, mock_templates, mock_mask, mock_metadata, mock_categories, mock_db_settings
    ):
        """Test that sensitive values are masked in settings page."""
        from app.views.settings import settings_page

        mock_db_settings.return_value = {"openai_api_key": "sk-real-secret"}
        mock_categories.return_value = {"AI": ["openai_api_key"]}
        mock_metadata.return_value = {"type": "str", "sensitive": True}
        mock_mask.return_value = "sk-****"
        mock_settings.version = "1.0.0"

        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        await settings_page(mock_request, mock_db)
        mock_mask.assert_called()


@pytest.mark.unit
class TestCredentialsPage:
    """Tests for credentials_page endpoint."""

    @patch("app.views.settings.get_all_settings_from_db")
    @patch("app.views.settings.SETTING_METADATA")
    @patch("app.views.settings.templates")
    @patch("app.views.settings.settings")
    @pytest.mark.asyncio
    async def test_credentials_page_success(self, mock_settings, mock_templates, mock_metadata, mock_db_settings):
        """Test credentials page renders successfully."""
        from app.views.settings import credentials_page

        mock_db_settings.return_value = {}
        mock_metadata.items.return_value = [
            ("openai_api_key", {"sensitive": True, "category": "AI", "description": "OpenAI key"}),
            ("workdir", {"sensitive": False, "category": "General", "description": "Work dir"}),
        ]
        mock_settings.openai_api_key = None
        mock_settings.version = "1.0.0"

        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        await credentials_page(mock_request, mock_db)
        mock_templates.TemplateResponse.assert_called_once()
        call_args = mock_templates.TemplateResponse.call_args
        assert call_args[0][0] == "credentials.html"

    @patch("app.views.settings.get_all_settings_from_db")
    @patch("app.views.settings.SETTING_METADATA")
    @patch("app.views.settings.templates")
    @patch("app.views.settings.settings")
    @pytest.mark.asyncio
    async def test_credentials_page_source_db(self, mock_settings, mock_templates, mock_metadata, mock_db_settings):
        """Test credentials page shows db source when credential is in database."""
        from app.views.settings import credentials_page

        mock_db_settings.return_value = {"openai_api_key": "sk-from-db"}
        mock_metadata.items.return_value = [
            ("openai_api_key", {"sensitive": True, "category": "AI", "description": "API key"}),
        ]
        mock_settings.openai_api_key = "sk-from-env"
        mock_settings.version = "1.0.0"

        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        await credentials_page(mock_request, mock_db)
        call_args = mock_templates.TemplateResponse.call_args
        context = call_args[0][1]
        ai_creds = context["categories"].get("AI", [])
        if ai_creds:
            assert ai_creds[0]["source"] == "db"
            assert ai_creds[0]["configured"] is True

    @patch("app.views.settings.get_all_settings_from_db")
    @patch("app.views.settings.SETTING_METADATA")
    @patch("app.views.settings.templates")
    @patch("app.views.settings.settings")
    @pytest.mark.asyncio
    async def test_credentials_page_source_env(self, mock_settings, mock_templates, mock_metadata, mock_db_settings):
        """Test credentials page shows env source when credential is only in env."""
        from app.views.settings import credentials_page

        mock_db_settings.return_value = {}
        mock_metadata.items.return_value = [
            ("openai_api_key", {"sensitive": True, "category": "AI", "description": "API key"}),
        ]
        mock_settings.openai_api_key = "sk-from-env"
        mock_settings.version = "1.0.0"

        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        await credentials_page(mock_request, mock_db)
        call_args = mock_templates.TemplateResponse.call_args
        context = call_args[0][1]
        ai_creds = context["categories"].get("AI", [])
        if ai_creds:
            assert ai_creds[0]["source"] == "env"

    @patch("app.views.settings.get_all_settings_from_db")
    @patch("app.views.settings.SETTING_METADATA")
    @patch("app.views.settings.templates")
    @patch("app.views.settings.settings")
    @pytest.mark.asyncio
    async def test_credentials_page_not_configured(
        self, mock_settings, mock_templates, mock_metadata, mock_db_settings
    ):
        """Test credentials page shows not configured when no credential value."""
        from app.views.settings import credentials_page

        mock_db_settings.return_value = {}
        mock_metadata.items.return_value = [
            ("openai_api_key", {"sensitive": True, "category": "AI", "description": "API key"}),
        ]
        mock_settings.openai_api_key = None
        mock_settings.version = "1.0.0"

        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        await credentials_page(mock_request, mock_db)
        call_args = mock_templates.TemplateResponse.call_args
        context = call_args[0][1]
        ai_creds = context["categories"].get("AI", [])
        if ai_creds:
            assert ai_creds[0]["source"] is None
            assert ai_creds[0]["configured"] is False

    @patch("app.views.settings.get_all_settings_from_db")
    @pytest.mark.asyncio
    async def test_credentials_page_raises_500_on_error(self, mock_db_settings):
        """Test credentials page raises 500 on error."""
        from fastapi import HTTPException

        from app.views.settings import credentials_page

        mock_db_settings.side_effect = Exception("DB error")
        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        with pytest.raises(HTTPException) as exc_info:
            await credentials_page(mock_request, mock_db)
        assert exc_info.value.status_code == 500

    def test_credentials_page_redirects_non_admin(self, client):
        """Test that non-admin users are redirected from credentials page."""
        response = client.get("/admin/credentials", follow_redirects=False)
        assert response.status_code == 302

    def test_credentials_page_accessible_with_admin(self, client):
        """Test credentials page is accessible with admin session."""
        client.cookies.set("session", _make_admin_session_cookie())
        response = client.get("/admin/credentials", follow_redirects=False)
        assert response.status_code == 200


@pytest.mark.unit
class TestAuditLogPage:
    """Tests for audit_log_page endpoint."""

    @patch("app.utils.settings_service.get_audit_log")
    @patch("app.views.settings.templates")
    @patch("app.views.settings.settings")
    @pytest.mark.asyncio
    async def test_audit_log_page_success(self, mock_settings, mock_templates, mock_audit_log):
        """Test audit log page renders successfully."""
        from app.views.settings import audit_log_page

        mock_audit_log.return_value = []
        mock_settings.version = "1.0.0"

        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        await audit_log_page(mock_request, mock_db)
        mock_templates.TemplateResponse.assert_called_once()
        call_args = mock_templates.TemplateResponse.call_args
        assert call_args[0][0] == "audit_log.html"

    @patch("app.utils.settings_service.get_audit_log")
    @patch("app.views.settings.templates")
    @patch("app.views.settings.settings")
    @pytest.mark.asyncio
    async def test_audit_log_page_with_entries(self, mock_settings, mock_templates, mock_audit_log):
        """Test audit log page with actual entries."""
        from app.views.settings import audit_log_page

        mock_entries = [
            {"key": "workdir", "old_value": "/old", "new_value": "/new", "changed_by": "admin"},
        ]
        mock_audit_log.return_value = mock_entries
        mock_settings.version = "1.0.0"

        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        await audit_log_page(mock_request, mock_db)
        call_args = mock_templates.TemplateResponse.call_args
        context = call_args[0][1]
        assert len(context["entries"]) == 1

    @patch("app.utils.settings_service.get_audit_log")
    @pytest.mark.asyncio
    async def test_audit_log_page_raises_500_on_error(self, mock_audit_log):
        """Test audit log page raises 500 on error."""
        from fastapi import HTTPException

        from app.views.settings import audit_log_page

        mock_audit_log.side_effect = Exception("DB error")
        mock_request = Mock()
        mock_request.session = {"user": {"id": "admin", "is_admin": True}}
        mock_db = Mock()

        with pytest.raises(HTTPException) as exc_info:
            await audit_log_page(mock_request, mock_db)
        assert exc_info.value.status_code == 500

    def test_audit_log_redirects_non_admin(self, client):
        """Test that non-admin users are redirected."""
        response = client.get("/admin/settings/audit-log", follow_redirects=False)
        assert response.status_code == 302

    def test_audit_log_accessible_with_admin(self, client):
        """Test audit log page is accessible with admin session."""
        client.cookies.set("session", _make_admin_session_cookie())
        response = client.get("/admin/settings/audit-log", follow_redirects=False)
        assert response.status_code == 200
