"""
Tests for ascend-langchain v2.0.0 migration from ascend-ai-sdk 1.0.0 to 2.1.0.

Verifies all five breaking changes are correctly handled:
1. OWKAIError exception class (not AscendError)
2. evaluate_action() replaces submit_action()
3. AgentAction no longer has tool_name field
4. Decision enum comparison replaces is_approved()/is_pending()
5. Client constructor uses api_url (not base_url) and accepts agent_id/agent_name
"""

import os
import sys
import pytest
from unittest.mock import MagicMock, patch, PropertyMock

# Add the SDK to the path so we can import ascend
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "sdk", "python"))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))


# ===== Test 1-3: Import tests =====

class TestImports:
    """Verify all imports work with v2.1.0 SDK."""

    def test_import_wrapper_module(self):
        """Test that wrapper module imports successfully."""
        from ascend_langchain.wrapper import AscendToolWrapper, wrap_tools
        assert AscendToolWrapper is not None
        assert wrap_tools is not None

    def test_import_callback_module(self):
        """Test that callback module imports successfully."""
        from ascend_langchain.callback import AscendCallbackHandler
        assert AscendCallbackHandler is not None

    def test_import_decorators_module(self):
        """Test that decorators module imports successfully."""
        from ascend_langchain.decorators import governed, governed_async
        assert governed is not None
        assert governed_async is not None

    def test_import_tools_module(self):
        """Test that tools module imports successfully."""
        from ascend_langchain.tools import GovernedBaseTool, GovernedStructuredTool, create_governed_tool
        assert GovernedBaseTool is not None
        assert GovernedStructuredTool is not None
        assert create_governed_tool is not None

    def test_import_top_level(self):
        """Test top-level package imports."""
        from ascend_langchain import (
            AscendToolWrapper,
            AscendCallbackHandler,
            governed,
            governed_async,
            GovernedBaseTool,
            GovernedStructuredTool,
            create_governed_tool,
        )
        assert AscendToolWrapper is not None

    def test_version_is_2_0_0(self):
        """Test package version is 2.0.0."""
        from ascend_langchain import __version__
        assert __version__ == "2.0.0"


# ===== Test 4-5: Exception handling =====

class TestExceptionHandling:
    """Verify OWKAIError is used instead of AscendError."""

    def test_owkai_error_imported_as_ascend_error(self):
        """Verify OWKAIError is aliased as AscendError internally."""
        from ascend.exceptions import OWKAIError
        # The wrapper module imports OWKAIError as AscendError
        from ascend_langchain.wrapper import AscendError as WrapperError
        assert WrapperError is OWKAIError

    def test_owkai_error_is_base_exception(self):
        """Verify OWKAIError is the base for all SDK exceptions."""
        from ascend.exceptions import (
            OWKAIError,
            AuthenticationError,
            AuthorizationError,
            ValidationError,
        )
        assert issubclass(AuthenticationError, OWKAIError)
        assert issubclass(AuthorizationError, OWKAIError)
        assert issubclass(ValidationError, OWKAIError)


# ===== Test 6-7: Decision enum =====

class TestDecisionEnum:
    """Verify Decision enum comparison works correctly."""

    def test_decision_enum_values(self):
        """Test Decision enum has correct values."""
        from ascend.models import Decision
        assert Decision.ALLOWED == "allowed"
        assert Decision.DENIED == "denied"
        assert Decision.PENDING == "pending"

    def test_authorization_decision_uses_decision_enum(self):
        """Test AuthorizationDecision.decision is a Decision enum."""
        from ascend.models import AuthorizationDecision, Decision
        decision = AuthorizationDecision(
            action_id="test-123",
            decision=Decision.ALLOWED,
        )
        assert decision.decision == Decision.ALLOWED
        assert decision.action_id == "test-123"


# ===== Helper: Real BaseTool subclass for testing =====

from langchain_core.tools import BaseTool as _BaseTool
from langchain_core.callbacks import CallbackManagerForToolRun as _CMFTR
from typing import Optional as _Opt


class _FakeTool(_BaseTool):
    """A minimal real BaseTool for use in tests."""
    name: str = "test_tool"
    description: str = "A test tool"

    def _run(self, tool_input: str, run_manager: _Opt[_CMFTR] = None) -> str:
        return "tool result"


# ===== Test 8-11: Wrapper construction and governance =====

class TestWrapperConstruction:
    """Test AscendToolWrapper with v2.1.0 API."""

    def _make_mock_tool(self, name="test_tool", description="A test tool"):
        """Create a real LangChain BaseTool for testing."""
        return _FakeTool(name=name, description=description)

    def test_wrapper_construction(self):
        """Test AscendToolWrapper can be constructed."""
        mock_tool = self._make_mock_tool()
        wrapper = AscendToolWrapper(
            tool=mock_tool,
            action_type="web.search",
            risk_level="low",
            api_key="owkai_test_key_123",
            api_url="https://api.owkai.app",
        )
        assert wrapper.action_type == "web.search"
        assert wrapper.risk_level == "low"
        assert wrapper.api_key == "owkai_test_key_123"
        assert wrapper.api_url == "https://api.owkai.app"

    def test_wrapper_uses_api_url_not_base_url(self):
        """Verify wrapper uses api_url parameter (v2.1.0), not base_url."""
        mock_tool = self._make_mock_tool()
        wrapper = AscendToolWrapper(
            tool=mock_tool,
            api_key="owkai_test_key_123",
            api_url="https://custom.api.url",
        )
        assert wrapper.api_url == "https://custom.api.url"
        assert not hasattr(wrapper, "base_url") or getattr(wrapper, "base_url", None) != "https://custom.api.url"

    @patch("ascend_langchain.wrapper.AscendClient")
    def test_wrapper_calls_evaluate_action_not_submit(self, mock_client_cls):
        """Verify wrapper calls evaluate_action, not submit_action."""
        from ascend.models import Decision, AuthorizationDecision

        mock_client = MagicMock()
        mock_decision = AuthorizationDecision(
            action_id="act-001",
            decision=Decision.ALLOWED,
            risk_score=25,
        )
        mock_client.evaluate_action.return_value = mock_decision
        mock_client_cls.return_value = mock_client

        mock_tool = self._make_mock_tool()
        wrapper = AscendToolWrapper(
            tool=mock_tool,
            action_type="web.search",
            api_key="owkai_test_key_123",
        )
        wrapper._client = mock_client

        result = wrapper._check_governance("test query")

        mock_client.evaluate_action.assert_called_once()
        assert not mock_client.submit_action.called
        assert result["approved"] is True
        assert result["action_id"] == "act-001"

    @patch("ascend_langchain.wrapper.AscendClient")
    def test_wrapper_tool_name_in_parameters(self, mock_client_cls):
        """Verify tool_name is passed in parameters dict, not as AgentAction field."""
        from ascend.models import Decision, AuthorizationDecision

        mock_client = MagicMock()
        mock_decision = AuthorizationDecision(
            action_id="act-002",
            decision=Decision.ALLOWED,
        )
        mock_client.evaluate_action.return_value = mock_decision
        mock_client_cls.return_value = mock_client

        mock_tool = self._make_mock_tool(name="my_search")
        wrapper = AscendToolWrapper(
            tool=mock_tool,
            action_type="web.search",
            api_key="owkai_test_key_123",
        )
        wrapper._client = mock_client

        wrapper._check_governance("test input")

        call_kwargs = mock_client.evaluate_action.call_args
        params = call_kwargs.kwargs.get("parameters", {})
        assert "tool_name" in params
        assert params["tool_name"] == "my_search"


# ===== Test 12-13: Fail-open and fail-closed paths =====

class TestFailModes:
    """Test fail_open=True and fail_open=False paths."""

    def _make_wrapper(self, fail_open: bool):
        from ascend.exceptions import OWKAIError
        mock_tool = _FakeTool(name="test_tool", description="test")
        wrapper = AscendToolWrapper(
            tool=mock_tool,
            api_key="owkai_test_key_123",
            fail_open=fail_open,
        )
        mock_client = MagicMock()
        mock_client.evaluate_action.side_effect = OWKAIError("Service unavailable")
        wrapper._client = mock_client
        return wrapper

    def test_fail_open_allows_on_error(self):
        """Test fail_open=True allows execution when governance errors occur."""
        wrapper = self._make_wrapper(fail_open=True)
        result = wrapper._check_governance("test")
        assert result["approved"] is True
        assert result["reason"] == "fail_open"

    def test_fail_closed_blocks_on_error(self):
        """Test fail_open=False blocks execution when governance errors occur."""
        wrapper = self._make_wrapper(fail_open=False)
        result = wrapper._check_governance("test")
        assert result["approved"] is False
        assert "Service unavailable" in result["reason"]


# ===== Test 14: Callback handler construction =====

class TestCallbackHandler:
    """Test AscendCallbackHandler with v2.1.0 API."""

    def test_callback_handler_construction(self):
        """Test callback handler uses api_url parameter."""
        handler = AscendCallbackHandler(
            agent_id="test-agent",
            agent_name="Test Agent",
            api_key="owkai_test_key_123",
            api_url="https://api.owkai.app",
        )
        assert handler.agent_id == "test-agent"
        assert handler.agent_name == "Test Agent"
        assert handler.api_url == "https://api.owkai.app"
        assert not hasattr(handler, "base_url")

    @patch("ascend_langchain.callback.AscendClient")
    def test_callback_calls_evaluate_action(self, mock_client_cls):
        """Test callback handler calls evaluate_action on tool start."""
        from ascend.models import Decision, AuthorizationDecision
        from uuid import uuid4

        mock_client = MagicMock()
        mock_decision = AuthorizationDecision(
            action_id="act-cb-001",
            decision=Decision.ALLOWED,
        )
        mock_client.evaluate_action.return_value = mock_decision
        mock_client_cls.return_value = mock_client

        handler = AscendCallbackHandler(
            agent_id="test-agent",
            api_key="owkai_test_key_123",
        )
        handler._client = mock_client

        handler.on_tool_start(
            serialized={"name": "search"},
            input_str="test query",
            run_id=uuid4(),
        )

        mock_client.evaluate_action.assert_called_once()
        assert not mock_client.submit_action.called


# ===== Test 15: Decorator application =====

class TestDecorators:
    """Test governed decorator with v2.1.0 API."""

    @patch("ascend_langchain.decorators._get_client")
    def test_governed_decorator_calls_evaluate_action(self, mock_get_client):
        """Test governed decorator calls evaluate_action, not submit_action."""
        from ascend.models import Decision, AuthorizationDecision
        from ascend_langchain.decorators import governed

        mock_client = MagicMock()
        mock_decision = AuthorizationDecision(
            action_id="act-dec-001",
            decision=Decision.ALLOWED,
        )
        mock_client.evaluate_action.return_value = mock_decision
        mock_get_client.return_value = mock_client

        @governed(action_type="database.query", tool_name="postgresql")
        def my_query(sql: str) -> str:
            return f"result of {sql}"

        result = my_query("SELECT 1")

        mock_client.evaluate_action.assert_called_once()
        assert not mock_client.submit_action.called
        assert result == "result of SELECT 1"

    @patch("ascend_langchain.decorators._get_client")
    def test_governed_decorator_denied(self, mock_get_client):
        """Test governed decorator raises PermissionError on DENIED."""
        from ascend.models import Decision, AuthorizationDecision
        from ascend_langchain.decorators import governed

        mock_client = MagicMock()
        mock_decision = AuthorizationDecision(
            action_id="act-dec-002",
            decision=Decision.DENIED,
            reason="Policy violation: too risky",
        )
        mock_client.evaluate_action.return_value = mock_decision
        mock_get_client.return_value = mock_client

        @governed(action_type="file.delete", tool_name="filesystem")
        def delete_all() -> str:
            return "deleted"

        with pytest.raises(PermissionError, match="denied"):
            delete_all()

    @patch("ascend_langchain.decorators._get_client")
    def test_governed_decorator_pending(self, mock_get_client):
        """Test governed decorator raises PermissionError on PENDING."""
        from ascend.models import Decision, AuthorizationDecision
        from ascend_langchain.decorators import governed

        mock_client = MagicMock()
        mock_decision = AuthorizationDecision(
            action_id="act-dec-003",
            decision=Decision.PENDING,
        )
        mock_client.evaluate_action.return_value = mock_decision
        mock_get_client.return_value = mock_client

        @governed(action_type="transaction.refund", tool_name="stripe")
        def process_refund() -> str:
            return "refunded"

        with pytest.raises(PermissionError, match="requires approval"):
            process_refund()

    @patch("ascend_langchain.decorators._get_client")
    def test_governed_decorator_tool_name_in_parameters(self, mock_get_client):
        """Test that tool_name goes into parameters, not as a top-level field."""
        from ascend.models import Decision, AuthorizationDecision
        from ascend_langchain.decorators import governed

        mock_client = MagicMock()
        mock_decision = AuthorizationDecision(
            action_id="act-dec-004",
            decision=Decision.ALLOWED,
        )
        mock_client.evaluate_action.return_value = mock_decision
        mock_get_client.return_value = mock_client

        @governed(action_type="database.query", tool_name="postgresql")
        def my_query(sql: str) -> str:
            return "ok"

        my_query("SELECT 1")

        call_kwargs = mock_client.evaluate_action.call_args
        params = call_kwargs.kwargs.get("parameters", {})
        assert params["tool_name"] == "postgresql"

    @patch("ascend_langchain.decorators._get_client")
    def test_governed_fail_open_on_owkai_error(self, mock_get_client):
        """Test fail_open=True with OWKAIError allows execution."""
        from ascend.exceptions import OWKAIError
        from ascend_langchain.decorators import governed

        mock_client = MagicMock()
        mock_client.evaluate_action.side_effect = OWKAIError("timeout")
        mock_get_client.return_value = mock_client

        @governed(action_type="api.call", tool_name="external", fail_open=True)
        def call_api() -> str:
            return "api result"

        result = call_api()
        assert result == "api result"


# ===== Test 20: Governed tool creation =====

class TestGovernedToolCreation:
    """Test create_governed_tool with v2.1.0 API."""

    @patch("ascend_langchain.tools.AscendClient")
    def test_create_governed_tool(self, mock_client_cls):
        """Test governed tool factory creates tool with evaluate_action."""
        from ascend.models import Decision, AuthorizationDecision
        from ascend_langchain.tools import create_governed_tool

        mock_client = MagicMock()
        mock_decision = AuthorizationDecision(
            action_id="act-tool-001",
            decision=Decision.ALLOWED,
        )
        mock_client.evaluate_action.return_value = mock_decision
        mock_client_cls.return_value = mock_client

        def my_func(query: str) -> str:
            return f"result: {query}"

        with patch.dict(os.environ, {"ASCEND_API_KEY": "owkai_test_key_123"}):
            tool = create_governed_tool(
                name="my_tool",
                description="A test tool",
                func=my_func,
                action_type="database.query",
                tool_name="postgresql",
            )

        assert tool.name == "my_tool"
        assert tool.description == "A test tool"


# Import at module level for test classes that reference them
from ascend_langchain.wrapper import AscendToolWrapper
from ascend_langchain.callback import AscendCallbackHandler
