"""Tests for access control integration with Odoo MCP module.

This module tests the AccessController class and its integration with
the Odoo MCP module's REST API endpoints.
"""

import json
import os
import socket
import urllib.error
from unittest.mock import MagicMock, patch

import pytest

from mcp_server_odoo.access_control import (
    AccessControlError,
    AccessController,
)
from mcp_server_odoo.config import OdooConfig


def is_odoo_server_running(host="localhost", port=8069):
    """Check if Odoo server is running."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(1)
    try:
        result = sock.connect_ex((host, port))
        return result == 0
    except Exception:
        return False
    finally:
        sock.close()


class TestAccessControl:
    """Test access control functionality."""

    @pytest.fixture
    def config(self):
        """Create test configuration."""
        return OdooConfig(
            url=os.getenv("ODOO_URL", "http://localhost:8069"),
            api_key="test_api_key",
            database=os.getenv("ODOO_DB"),
        )

    @pytest.fixture
    def controller(self, config):
        """Create AccessController instance."""
        return AccessController(config, cache_ttl=60)

    def test_init_without_api_key(self):
        """Test initialization fails without API key."""
        config = OdooConfig(
            url=os.getenv("ODOO_URL", "http://localhost:8069"),
            username=os.getenv("ODOO_USER", "admin"),
            password=os.getenv("ODOO_PASSWORD", "admin"),
            database=os.getenv("ODOO_DB"),
        )

        with pytest.raises(AccessControlError, match="API key required"):
            AccessController(config)

    @patch("urllib.request.urlopen")
    def test_make_request_success(self, mock_urlopen, controller):
        """Test successful REST API request."""
        # Mock response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {"success": True, "data": {"test": "value"}}
        ).encode("utf-8")
        mock_urlopen.return_value.__enter__.return_value = mock_response

        # Make request
        result = controller._make_request("/test/endpoint")

        assert result["success"] is True
        assert result["data"]["test"] == "value"

    @patch("urllib.request.urlopen")
    def test_make_request_api_error(self, mock_urlopen, controller):
        """Test REST API request with API error response."""
        # Mock error response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {"success": False, "error": {"message": "Test error"}}
        ).encode("utf-8")
        mock_urlopen.return_value.__enter__.return_value = mock_response

        # Should raise error
        with pytest.raises(AccessControlError, match="API error: Test error"):
            controller._make_request("/test/endpoint")

    @patch("urllib.request.urlopen")
    def test_make_request_http_401(self, mock_urlopen, controller):
        """Test REST API request with 401 error."""
        mock_urlopen.side_effect = urllib.error.HTTPError(None, 401, "Unauthorized", {}, None)

        with pytest.raises(AccessControlError, match="Invalid API key"):
            controller._make_request("/test/endpoint")

    @patch("urllib.request.urlopen")
    def test_make_request_http_404(self, mock_urlopen, controller):
        """Test REST API request with 404 error."""
        mock_urlopen.side_effect = urllib.error.HTTPError(None, 404, "Not Found", {}, None)

        with pytest.raises(AccessControlError, match="Endpoint not found"):
            controller._make_request("/test/endpoint")

    def test_cache_operations(self, controller):
        """Test cache get/set operations."""
        # Test cache miss
        assert controller._get_from_cache("test_key") is None

        # Test cache set and hit
        controller._set_cache("test_key", {"data": "value"})
        assert controller._get_from_cache("test_key") == {"data": "value"}

        # Test cache clear
        controller.clear_cache()
        assert controller._get_from_cache("test_key") is None

    def test_cache_expiration(self, controller):
        """Test cache expiration."""
        # Set cache with short TTL
        controller.cache_ttl = 0  # Immediate expiration
        controller._set_cache("test_key", "value")

        # Should be expired
        assert controller._get_from_cache("test_key") is None

    @patch("urllib.request.urlopen")
    def test_get_enabled_models(self, mock_urlopen, controller):
        """Test getting enabled models list."""
        # Mock response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {
                "success": True,
                "data": {
                    "models": [
                        {"model": "res.partner", "name": "Contact"},
                        {"model": "res.users", "name": "Users"},
                    ]
                },
            }
        ).encode("utf-8")
        mock_urlopen.return_value.__enter__.return_value = mock_response

        # Get models
        models = controller.get_enabled_models()

        assert len(models) == 2
        assert models[0]["model"] == "res.partner"
        assert models[1]["name"] == "Users"

        # Second call should use cache
        models2 = controller.get_enabled_models()
        assert models2 == models
        mock_urlopen.assert_called_once()  # Only called once due to cache

    @patch("urllib.request.urlopen")
    def test_is_model_enabled(self, mock_urlopen, controller):
        """Test checking if model is enabled."""
        # Mock response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {
                "success": True,
                "data": {
                    "models": [
                        {"model": "res.partner", "name": "Contact"},
                        {"model": "res.users", "name": "Users"},
                    ]
                },
            }
        ).encode("utf-8")
        mock_urlopen.return_value.__enter__.return_value = mock_response

        # Check models
        assert controller.is_model_enabled("res.partner") is True
        assert controller.is_model_enabled("res.users") is True
        assert controller.is_model_enabled("account.move") is False

    @patch("urllib.request.urlopen")
    def test_get_model_permissions(self, mock_urlopen, controller):
        """Test getting model permissions."""
        # Mock response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {
                "success": True,
                "data": {
                    "model": "res.partner",
                    "enabled": True,
                    "operations": {"read": True, "write": True, "create": False, "unlink": False},
                },
            }
        ).encode("utf-8")
        mock_urlopen.return_value.__enter__.return_value = mock_response

        # Get permissions
        perms = controller.get_model_permissions("res.partner")

        assert perms.model == "res.partner"
        assert perms.enabled is True
        assert perms.can_read is True
        assert perms.can_write is True
        assert perms.can_create is False
        assert perms.can_unlink is False

        # Test can_perform method
        assert perms.can_perform("read") is True
        assert perms.can_perform("write") is True
        assert perms.can_perform("create") is False
        assert perms.can_perform("delete") is False  # Alias for unlink

    @patch("urllib.request.urlopen")
    def test_check_operation_allowed(self, mock_urlopen, controller):
        """Test checking if operation is allowed."""
        # Mock response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {
                "success": True,
                "data": {
                    "model": "res.partner",
                    "enabled": True,
                    "operations": {"read": True, "write": False, "create": False, "unlink": False},
                },
            }
        ).encode("utf-8")
        mock_urlopen.return_value.__enter__.return_value = mock_response

        # Check operations
        allowed, msg = controller.check_operation_allowed("res.partner", "read")
        assert allowed is True
        assert msg is None

        allowed, msg = controller.check_operation_allowed("res.partner", "write")
        assert allowed is False
        assert "Operation 'write' not allowed" in msg

    @patch("urllib.request.urlopen")
    def test_check_operation_model_disabled(self, mock_urlopen, controller):
        """Test checking operation on disabled model."""
        # Mock response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {"success": True, "data": {"model": "res.partner", "enabled": False, "operations": {}}}
        ).encode("utf-8")
        mock_urlopen.return_value.__enter__.return_value = mock_response

        # Check operation
        allowed, msg = controller.check_operation_allowed("res.partner", "read")
        assert allowed is False
        assert "not enabled for MCP access" in msg

    @patch("urllib.request.urlopen")
    def test_validate_model_access(self, mock_urlopen, controller):
        """Test validate_model_access method."""
        # Mock allowed response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {
                "success": True,
                "data": {"model": "res.partner", "enabled": True, "operations": {"read": True}},
            }
        ).encode("utf-8")
        mock_urlopen.return_value.__enter__.return_value = mock_response

        # Should not raise for allowed operation
        controller.validate_model_access("res.partner", "read")

        # Mock denied response
        mock_response.read.return_value = json.dumps(
            {
                "success": True,
                "data": {"model": "res.partner", "enabled": True, "operations": {"read": False}},
            }
        ).encode("utf-8")

        # Clear cache to force new request
        controller.clear_cache()

        # Should raise for denied operation
        with pytest.raises(AccessControlError):
            controller.validate_model_access("res.partner", "read")

    @patch("urllib.request.urlopen")
    def test_filter_enabled_models(self, mock_urlopen, controller):
        """Test filtering enabled models."""
        # Mock response
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {
                "success": True,
                "data": {
                    "models": [
                        {"model": "res.partner", "name": "Contact"},
                        {"model": "res.users", "name": "Users"},
                    ]
                },
            }
        ).encode("utf-8")
        mock_urlopen.return_value.__enter__.return_value = mock_response

        # Filter models
        models = ["res.partner", "account.move", "res.users", "stock.picking"]
        filtered = controller.filter_enabled_models(models)

        assert filtered == ["res.partner", "res.users"]

    @patch("urllib.request.urlopen")
    def test_get_all_permissions(self, mock_urlopen, controller):
        """Test getting permissions for all models."""
        # Mock models list response
        models_response = MagicMock()
        models_response.read.return_value = json.dumps(
            {
                "success": True,
                "data": {
                    "models": [
                        {"model": "res.partner", "name": "Contact"},
                        {"model": "res.users", "name": "Users"},
                    ]
                },
            }
        ).encode("utf-8")

        # Mock permissions responses
        partner_response = MagicMock()
        partner_response.read.return_value = json.dumps(
            {
                "success": True,
                "data": {
                    "model": "res.partner",
                    "enabled": True,
                    "operations": {"read": True, "write": True},
                },
            }
        ).encode("utf-8")

        users_response = MagicMock()
        users_response.read.return_value = json.dumps(
            {
                "success": True,
                "data": {
                    "model": "res.users",
                    "enabled": True,
                    "operations": {"read": True, "write": False},
                },
            }
        ).encode("utf-8")

        # Configure mock to return different responses
        mock_urlopen.return_value.__enter__.side_effect = [
            models_response,
            partner_response,
            users_response,
        ]

        # Get all permissions
        all_perms = controller.get_all_permissions()

        assert len(all_perms) == 2
        assert all_perms["res.partner"].can_write is True
        assert all_perms["res.users"].can_write is False


@pytest.mark.skipif(
    not is_odoo_server_running(), reason="Odoo server not running at localhost:8069"
)
class TestAccessControlIntegration:
    """Integration tests with real Odoo server."""

    @pytest.fixture
    def real_config(self):
        """Create configuration with real credentials."""
        return OdooConfig(
            url=os.getenv("ODOO_URL", "http://localhost:8069"),
            api_key=os.getenv("ODOO_API_KEY"),
            database=os.getenv("ODOO_DB"),
        )

    def test_real_get_enabled_models(self, real_config):
        """Test getting enabled models from real server."""
        controller = AccessController(real_config)

        models = controller.get_enabled_models()

        assert isinstance(models, list)
        print(f"Found {len(models)} enabled models")

        # Just verify we got some models
        if models:
            # Print first few models as example
            for model in models[:3]:
                print(f"  - {model.get('model', 'unknown')}")

    def test_real_model_permissions(self, real_config, readable_model):
        """Test getting model permissions from real server."""
        controller = AccessController(real_config)

        # Use the discovered readable model
        model_name = readable_model.model

        # Get model permissions
        perms = controller.get_model_permissions(model_name)

        assert perms.model == model_name
        assert perms.enabled is True
        assert perms.can_read is True  # We specifically requested a readable model
        print(
            f"{model_name} permissions: read={perms.can_read}, "
            f"write={perms.can_write}, create={perms.can_create}, "
            f"unlink={perms.can_unlink}"
        )

    def test_real_check_operations(self, real_config, readable_model, disabled_model):
        """Test checking operations on real server."""
        controller = AccessController(real_config)

        # Check enabled model operations
        allowed, msg = controller.check_operation_allowed(readable_model.model, "read")
        print(f"{readable_model.model} read: allowed={allowed}, msg={msg}")
        assert allowed is True

        # Check a model we know is not enabled
        allowed, msg = controller.check_operation_allowed(disabled_model, "read")
        print(f"{disabled_model} read: allowed={allowed}, msg={msg}")
        assert allowed is False

    def test_real_validate_access(self, real_config, readable_model, disabled_model):
        """Test access validation on real server."""
        controller = AccessController(real_config)

        # Should not raise for enabled model with permission
        try:
            controller.validate_model_access(readable_model.model, "read")
            print(f"{readable_model.model} read access validated")
        except AccessControlError as e:
            print(f"{readable_model.model} read access denied: {e}")

        # Should raise for non-enabled model
        with pytest.raises(AccessControlError):
            controller.validate_model_access(disabled_model, "read")

    def test_real_cache_performance(self, real_config):
        """Test cache improves performance."""
        controller = AccessController(real_config)

        import time

        # First call - no cache
        start = time.time()
        models1 = controller.get_enabled_models()
        time1 = time.time() - start

        # Second call - from cache
        start = time.time()
        models2 = controller.get_enabled_models()
        time2 = time.time() - start

        assert models1 == models2
        assert time2 < time1  # Cache should be faster
        print(f"First call: {time1:.3f}s, Cached call: {time2:.3f}s")

    def test_real_all_permissions(self, real_config):
        """Test getting all permissions from real server."""
        controller = AccessController(real_config)

        all_perms = controller.get_all_permissions()

        print(f"Retrieved permissions for {len(all_perms)} models")

        # Print a sample
        for model, perms in list(all_perms.items())[:3]:
            print(f"{model}: read={perms.can_read}, write={perms.can_write}")


if __name__ == "__main__":
    # Run integration tests when executed directly
    pytest.main([__file__, "-v", "-k", "Integration"])
