#!/usr/bin/env python3
"""
Tests for STORY-002: UnifiedQwenClient Core
============================================

Black Box Tests:
- Singleton returns same instance
- generate_sync returns QwenResponse
- Correct endpoint used (NOT localhost)

White Box Tests:
- Thread safety under concurrent access
- Request building
- Response parsing
- Usage logging
"""

import os
import sys
import json
import threading
import unittest
from unittest.mock import patch, MagicMock, mock_open
from concurrent.futures import ThreadPoolExecutor

# Add project root to path
sys.path.insert(0, "/mnt/e/genesis-system")

from core.qwen.unified_client import UnifiedQwenClient, QwenResponse, get_qwen_client
from core.qwen.config import QwenConfig
from core.qwen.exceptions import QwenConnectionError, QwenTimeoutError, QwenResponseError


class TestUnifiedQwenClientBlackBox(unittest.TestCase):
    """Black box tests - test behavior without internal knowledge."""

    def setUp(self):
        """Reset singleton before each test."""
        UnifiedQwenClient.reset_singleton()

    def tearDown(self):
        """Reset singleton after each test."""
        UnifiedQwenClient.reset_singleton()

    def test_singleton_returns_same_instance(self):
        """Test that multiple instantiations return same object."""
        client1 = UnifiedQwenClient()
        client2 = UnifiedQwenClient()

        self.assertIs(client1, client2)
        self.assertEqual(id(client1), id(client2))

    def test_get_qwen_client_returns_singleton(self):
        """Test convenience function returns singleton."""
        client1 = get_qwen_client()
        client2 = get_qwen_client()

        self.assertIs(client1, client2)

    def test_client_has_correct_endpoint(self):
        """CRITICAL: Verify client uses AIVA endpoint, not localhost."""
        client = UnifiedQwenClient()

        # Must NOT be localhost
        self.assertNotIn("localhost", client.config.base_url)
        self.assertNotIn("127.0.0.1", client.config.base_url)

        # Must be AIVA's server
        self.assertIn("152.53.201.152", client.config.base_url)
        self.assertIn("23405", client.config.base_url)

    def test_qwen_response_has_required_fields(self):
        """Test QwenResponse dataclass has all required fields."""
        response = QwenResponse(
            text="Hello",
            model="test-model",
            tokens_used=10,
            execution_time=1.5
        )

        self.assertEqual(response.text, "Hello")
        self.assertEqual(response.model, "test-model")
        self.assertEqual(response.tokens_used, 10)
        self.assertTrue(response.success)
        self.assertEqual(response.cost_estimate, 0.0)  # Ollama is free

    def test_client_has_generate_sync_method(self):
        """Test client exposes generate_sync method."""
        client = UnifiedQwenClient()

        self.assertTrue(hasattr(client, "generate_sync"))
        self.assertTrue(callable(client.generate_sync))

    def test_client_has_chat_sync_method(self):
        """Test client exposes chat_sync method."""
        client = UnifiedQwenClient()

        self.assertTrue(hasattr(client, "chat_sync"))
        self.assertTrue(callable(client.chat_sync))

    def test_client_has_ping_method(self):
        """Test client exposes ping method for warmth checks."""
        client = UnifiedQwenClient()

        self.assertTrue(hasattr(client, "ping"))
        self.assertTrue(callable(client.ping))

    def test_client_has_status_method(self):
        """Test client exposes get_status method."""
        client = UnifiedQwenClient()

        self.assertTrue(hasattr(client, "get_status"))
        self.assertTrue(callable(client.get_status))


class TestUnifiedQwenClientWhiteBox(unittest.TestCase):
    """White box tests - test internal implementation."""

    def setUp(self):
        """Reset singleton before each test."""
        UnifiedQwenClient.reset_singleton()

    def tearDown(self):
        """Reset singleton after each test."""
        UnifiedQwenClient.reset_singleton()

    def test_singleton_is_thread_safe(self):
        """Test singleton pattern under concurrent access."""
        instances = []
        errors = []

        def create_instance():
            try:
                instance = UnifiedQwenClient()
                instances.append(id(instance))
            except Exception as e:
                errors.append(e)

        # Create many instances concurrently
        with ThreadPoolExecutor(max_workers=10) as executor:
            futures = [executor.submit(create_instance) for _ in range(50)]
            for future in futures:
                future.result()

        # All should be same instance
        self.assertEqual(len(errors), 0)
        self.assertTrue(len(instances) > 0)
        self.assertEqual(len(set(instances)), 1)  # All same ID

    def test_initialized_flag_prevents_reinit(self):
        """Test that _initialized flag prevents double initialization."""
        client1 = UnifiedQwenClient()
        original_config = client1.config

        # Get again - should not reinitialize
        client2 = UnifiedQwenClient()

        self.assertIs(client1.config, client2.config)

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_make_request_builds_correct_request(self, mock_urlopen):
        """Test _make_request builds proper HTTP request."""
        mock_response = MagicMock()
        mock_response.read.return_value = b'{"response": "test"}'
        mock_response.__enter__ = MagicMock(return_value=mock_response)
        mock_response.__exit__ = MagicMock(return_value=False)
        mock_urlopen.return_value = mock_response

        client = UnifiedQwenClient()
        result = client._make_request(
            "http://test.com/api",
            {"key": "value"},
            timeout=10.0
        )

        # Verify urlopen was called
        mock_urlopen.assert_called_once()
        call_args = mock_urlopen.call_args

        # Check timeout
        self.assertEqual(call_args.kwargs.get("timeout"), 10.0)

        # Check result
        self.assertEqual(result, {"response": "test"})

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_generate_sync_returns_qwen_response(self, mock_urlopen):
        """Test generate_sync returns proper QwenResponse."""
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps({
            "response": "4",
            "model": "test-model",
            "prompt_eval_count": 5,
            "eval_count": 2
        }).encode()
        mock_response.__enter__ = MagicMock(return_value=mock_response)
        mock_response.__exit__ = MagicMock(return_value=False)
        mock_urlopen.return_value = mock_response

        client = UnifiedQwenClient()
        response = client.generate_sync("What is 2+2?")

        self.assertIsInstance(response, QwenResponse)
        self.assertEqual(response.text, "4")
        self.assertEqual(response.tokens_used, 7)
        self.assertTrue(response.success)

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_generate_sync_includes_options(self, mock_urlopen):
        """Test generate_sync passes options correctly."""
        mock_response = MagicMock()
        mock_response.read.return_value = b'{"response": "test"}'
        mock_response.__enter__ = MagicMock(return_value=mock_response)
        mock_response.__exit__ = MagicMock(return_value=False)
        mock_urlopen.return_value = mock_response

        client = UnifiedQwenClient()
        client.generate_sync(
            "test",
            temperature=0.5,
            max_tokens=100,
            system_prompt="You are helpful"
        )

        # Verify request was made
        mock_urlopen.assert_called_once()
        call_args = mock_urlopen.call_args
        request = call_args[0][0]

        # Parse the request body
        body = json.loads(request.data.decode())
        self.assertEqual(body["options"]["temperature"], 0.5)
        self.assertEqual(body["options"]["num_predict"], 100)
        self.assertEqual(body["system"], "You are helpful")

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_connection_error_raised_on_url_error(self, mock_urlopen):
        """Test QwenConnectionError raised on URL error."""
        mock_urlopen.side_effect = urllib.error.URLError("Connection refused")

        client = UnifiedQwenClient()

        with self.assertRaises(QwenConnectionError):
            client.generate_sync("test")

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_timeout_error_raised_on_timeout(self, mock_urlopen):
        """Test QwenTimeoutError raised on timeout."""
        mock_urlopen.side_effect = urllib.error.URLError("timed out")

        client = UnifiedQwenClient()

        with self.assertRaises(QwenTimeoutError):
            client.generate_sync("test")

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_response_error_on_invalid_json(self, mock_urlopen):
        """Test QwenResponseError raised on invalid JSON."""
        mock_response = MagicMock()
        mock_response.read.return_value = b"not valid json"
        mock_response.__enter__ = MagicMock(return_value=mock_response)
        mock_response.__exit__ = MagicMock(return_value=False)
        mock_urlopen.return_value = mock_response

        client = UnifiedQwenClient()

        with self.assertRaises(QwenResponseError):
            client.generate_sync("test")

    @patch("builtins.open", new_callable=mock_open)
    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_usage_logged_to_jsonl(self, mock_urlopen, mock_file):
        """Test that usage is logged to JSONL file."""
        mock_response = MagicMock()
        mock_response.read.return_value = b'{"response": "test"}'
        mock_response.__enter__ = MagicMock(return_value=mock_response)
        mock_response.__exit__ = MagicMock(return_value=False)
        mock_urlopen.return_value = mock_response

        client = UnifiedQwenClient()
        client.generate_sync("test prompt")

        # Verify file was opened for append
        mock_file.assert_called()
        # Verify JSON was written
        handle = mock_file()
        handle.write.assert_called()
        written = handle.write.call_args[0][0]
        self.assertIn("timestamp", written)
        self.assertIn("model", written)

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_chat_sync_uses_chat_endpoint(self, mock_urlopen):
        """Test chat_sync uses the chat API endpoint."""
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps({
            "message": {"content": "Hello!"},
            "model": "test"
        }).encode()
        mock_response.__enter__ = MagicMock(return_value=mock_response)
        mock_response.__exit__ = MagicMock(return_value=False)
        mock_urlopen.return_value = mock_response

        client = UnifiedQwenClient()
        response = client.chat_sync([
            {"role": "user", "content": "Hi"}
        ])

        # Verify chat endpoint was used
        call_args = mock_urlopen.call_args
        request = call_args[0][0]
        self.assertIn("/api/chat", request.full_url)

        self.assertEqual(response.text, "Hello!")


class TestQwenResponse(unittest.TestCase):
    """Tests for QwenResponse dataclass."""

    def test_cost_estimate_is_zero(self):
        """Test Ollama cost is always zero (free)."""
        response = QwenResponse(text="test", model="test")
        self.assertEqual(response.cost_estimate, 0.0)

    def test_default_values(self):
        """Test default values are sensible."""
        response = QwenResponse(text="test", model="test")

        self.assertEqual(response.tokens_used, 0)
        self.assertEqual(response.execution_time, 0.0)
        self.assertTrue(response.success)
        self.assertIsNone(response.thinking)
        self.assertEqual(response.raw_response, {})


# Import urllib for exception
import urllib.error


if __name__ == "__main__":
    unittest.main(verbosity=2)
