"""
Tests for the NeuroShard HTTP REST API endpoints.

Tests the /api/v1/* endpoints defined in runner.py.
Uses FastAPI's TestClient for testing without running a server.

Run with:
    python -m unittest tests/test_api_endpoints.py -v

Requirements:
    pip install fastapi httpx
"""

import sys
import os
import unittest
from unittest.mock import Mock, patch, MagicMock
import json
import tempfile

# Add parent directory to path
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))

# Check if fastapi is available
try:
    from fastapi.testclient import TestClient
    FASTAPI_AVAILABLE = True
except ImportError:
    FASTAPI_AVAILABLE = False
    TestClient = None

# Mock torch before importing runner to avoid CUDA issues in tests
mock_torch = MagicMock()
mock_torch.__version__ = "2.0.0"
mock_torch.cuda.is_available.return_value = False
sys.modules['torch'] = mock_torch
sys.modules['torch.nn'] = MagicMock()
sys.modules['torch.cuda'] = MagicMock()


def skipIfNoFastAPI(cls):
    """Class decorator to skip tests if FastAPI is not available."""
    if not FASTAPI_AVAILABLE:
        return unittest.skip("FastAPI not installed")(cls)
    return cls


@skipIfNoFastAPI
class TestAPIEndpoints(unittest.TestCase):
    """
    Tests for HTTP REST API endpoints.
    
    These tests mock the global NEURO_NODE and P2P objects to test
    endpoint behavior without requiring actual model/network setup.
    """

    @classmethod
    def setUpClass(cls):
        """Set up test fixtures once for all tests."""
        # Import runner after mocking torch
        from neuroshard import runner
        cls.runner = runner
        
        # Create test client
        cls.client = TestClient(runner.node_app)
        
    def setUp(self):
        """Set up mocks before each test."""
        # Mock NEURO_NODE
        self.mock_node = MagicMock()
        self.mock_node.node_id = "test_node_123"
        self.mock_node.my_layer_ids = [4, 5, 6, 7]
        self.mock_node.enable_training = True
        self.mock_node.model = MagicMock()
        self.mock_node.model.has_embedding = False
        self.mock_node.model.has_lm_head = False
        
        # Mock stats
        self.mock_node.get_stats.return_value = {
            "total_tokens_processed": 10000,
            "total_training_rounds": 100,
            "current_loss": 2.5,
        }
        
        # Mock P2P
        self.mock_p2p = MagicMock()
        self.mock_p2p.ledger = MagicMock()
        self.mock_p2p.ledger.get_balance.return_value = 1000.0
        self.mock_p2p.ledger.stake = 500.0
        self.mock_p2p.ledger.stake_multiplier = 1.5
        self.mock_p2p.ledger.pending_rewards = 50.0
        self.mock_p2p.ledger.pending_transactions = []
        
        # Mock peers
        self.mock_p2p.peers = {
            "peer_1": {
                "node_id": "peer_1",
                "url": "http://192.168.1.100:8000",
                "grpc_addr": "192.168.1.100:9000",
                "role": "worker",
                "layers": [0, 1, 2, 3],
                "last_seen": 1733400000.0,
            },
            "peer_2": {
                "node_id": "peer_2",
                "url": "http://192.168.1.101:8000",
                "grpc_addr": "192.168.1.101:9000",
                "role": "validator",
                "layers": [28, 29, 30, 31],
                "last_seen": 1733400000.0,
            }
        }
        
        # Patch globals
        self.node_patcher = patch.object(self.runner, 'NEURO_NODE', self.mock_node)
        self.p2p_patcher = patch.object(self.runner, 'P2P', self.mock_p2p)
        self.start_patcher = patch.object(self.runner, 'START_TIME', 1733396400.0)
        
        self.node_patcher.start()
        self.p2p_patcher.start()
        self.start_patcher.start()

    def tearDown(self):
        """Clean up mocks after each test."""
        self.node_patcher.stop()
        self.p2p_patcher.stop()
        self.start_patcher.stop()

    # ==================== STATUS ENDPOINTS ====================

    def test_get_status_v1(self):
        """Test GET /api/v1/status endpoint."""
        with patch('time.time', return_value=1733400000.0):
            with patch('psutil.cpu_percent', return_value=45.0):
                with patch('psutil.virtual_memory') as mock_mem:
                    mock_mem.return_value = MagicMock(used=4000000000, total=16000000000)
                    
                    response = self.client.get("/api/v1/status")
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertEqual(data["node_id"], "test_node_123")
        self.assertEqual(data["status"], "running")
        self.assertEqual(data["layers"], [4, 5, 6, 7])
        self.assertEqual(data["peer_count"], 2)
        self.assertIn("training", data)
        self.assertIn("resources", data)

    def test_get_status_v1_no_node(self):
        """Test GET /api/v1/status when node not initialized."""
        self.node_patcher.stop()
        with patch.object(self.runner, 'NEURO_NODE', None):
            response = self.client.get("/api/v1/status")
        self.node_patcher.start()
        
        self.assertEqual(response.status_code, 503)

    def test_get_metrics_v1(self):
        """Test GET /api/v1/metrics endpoint."""
        self.mock_p2p.ledger.get_daily_rewards.return_value = 100.5
        self.mock_p2p.ledger.get_total_rewards.return_value = 5000.0
        
        response = self.client.get("/api/v1/metrics")
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertIn("timestamp", data)
        self.assertIn("inference", data)
        self.assertIn("training", data)
        self.assertIn("network", data)
        self.assertIn("rewards", data)

    def test_get_health_v1(self):
        """Test GET /api/v1/health endpoint."""
        response = self.client.get("/api/v1/health")
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertTrue(data["healthy"])
        self.assertIn("checks", data)

    # ==================== INFERENCE ENDPOINTS ====================

    def test_post_inference_v1(self):
        """Test POST /api/v1/inference endpoint."""
        # Mock the inference
        self.mock_node.inference = MagicMock(return_value="Generated text response")
        
        response = self.client.post(
            "/api/v1/inference",
            json={
                "prompt": "Hello, AI!",
                "max_tokens": 50,
                "temperature": 0.7,
            }
        )
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertIn("id", data)
        self.assertIn("text", data)
        self.assertIn("tokens_generated", data)
        self.assertIn("finish_reason", data)
        self.assertIn("usage", data)
        self.assertIn("timing", data)

    def test_post_inference_v1_empty_prompt(self):
        """Test POST /api/v1/inference with empty prompt."""
        response = self.client.post(
            "/api/v1/inference",
            json={
                "prompt": "",
                "max_tokens": 50,
            }
        )
        
        # Should return error for empty prompt
        self.assertIn(response.status_code, [400, 422])

    # ==================== WALLET ENDPOINTS ====================

    def test_get_wallet_balance_v1(self):
        """Test GET /api/v1/wallet/balance endpoint."""
        response = self.client.get("/api/v1/wallet/balance")
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertIn("address", data)
        self.assertIn("balances", data)
        self.assertIn("available", data["balances"])
        self.assertIn("staked", data["balances"])
        self.assertIn("pending", data["balances"])
        self.assertIn("total", data["balances"])

    def test_post_wallet_send_v1(self):
        """Test POST /api/v1/wallet/send endpoint."""
        self.mock_p2p.ledger.send.return_value = {
            "tx_hash": "tx_123",
            "success": True,
        }
        
        response = self.client.post(
            "/api/v1/wallet/send",
            json={
                "to": "recipient_node_id",
                "amount": 100.0,
                "memo": "Test payment"
            }
        )
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertIn("transaction_id", data)
        self.assertIn("amount", data)
        self.assertEqual(data["amount"], 100.0)

    def test_post_wallet_send_v1_insufficient_balance(self):
        """Test POST /api/v1/wallet/send with insufficient balance."""
        # Set available balance lower than send amount
        self.mock_p2p.ledger.get_balance.return_value = 50.0
        
        response = self.client.post(
            "/api/v1/wallet/send",
            json={
                "to": "recipient_node_id",
                "amount": 100.0,
            }
        )
        
        # Should fail with insufficient balance
        self.assertIn(response.status_code, [400, 402])

    def test_get_wallet_transactions_v1(self):
        """Test GET /api/v1/wallet/transactions endpoint."""
        self.mock_p2p.ledger.get_transaction_history.return_value = [
            {
                "id": "tx_1",
                "type": "reward",
                "amount": 10.0,
                "timestamp": 1733400000.0,
            },
            {
                "id": "tx_2",
                "type": "send",
                "amount": -50.0,
                "to": "peer_1",
                "timestamp": 1733396400.0,
            }
        ]
        
        response = self.client.get("/api/v1/wallet/transactions?limit=10")
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertIn("transactions", data)
        self.assertIsInstance(data["transactions"], list)

    def test_post_wallet_stake_v1(self):
        """Test POST /api/v1/wallet/stake endpoint."""
        self.mock_p2p.ledger.stake_tokens.return_value = True
        
        response = self.client.post(
            "/api/v1/wallet/stake",
            json={
                "amount": 1000.0,
                "duration_days": 30
            }
        )
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertTrue(data["success"])
        self.assertIn("stake", data)

    def test_post_wallet_unstake_v1(self):
        """Test POST /api/v1/wallet/unstake endpoint."""
        self.mock_p2p.ledger.unstake_tokens.return_value = True
        
        response = self.client.post(
            "/api/v1/wallet/unstake",
            json={
                "amount": 500.0
            }
        )
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertTrue(data["success"])
        self.assertIn("unstake", data)

    def test_get_wallet_rewards_v1(self):
        """Test GET /api/v1/wallet/rewards endpoint."""
        self.mock_p2p.ledger.get_reward_history.return_value = [
            {"date": "2024-12-05", "amount": 100.0, "proofs": 50},
            {"date": "2024-12-04", "amount": 90.0, "proofs": 45},
        ]
        
        response = self.client.get("/api/v1/wallet/rewards")
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertIn("total", data)
        self.assertIn("by_day", data)

    # ==================== NETWORK ENDPOINTS ====================

    def test_get_peers_v1(self):
        """Test GET /api/v1/peers endpoint."""
        response = self.client.get("/api/v1/peers")
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertIn("peers", data)
        self.assertEqual(len(data["peers"]), 2)
        
        peer = data["peers"][0]
        self.assertIn("id", peer)
        self.assertIn("address", peer)
        self.assertIn("role", peer)
        self.assertIn("layers", peer)

    def test_get_layers_v1(self):
        """Test GET /api/v1/layers endpoint."""
        response = self.client.get("/api/v1/layers")
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertIn("layers", data)
        self.assertIn("my_layer_count", data)
        self.assertEqual(data["my_layer_count"], 4)

    # ==================== CONFIG ENDPOINTS ====================

    def test_get_config_v1(self):
        """Test GET /api/v1/config endpoint."""
        response = self.client.get("/api/v1/config")
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertIn("node_id", data)
        self.assertIn("port", data)
        self.assertIn("grpc_port", data)
        self.assertIn("training", data)

    def test_patch_config_v1(self):
        """Test PATCH /api/v1/config endpoint."""
        response = self.client.patch(
            "/api/v1/config",
            json={
                "training": {
                    "batch_size": 16
                }
            }
        )
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        
        self.assertTrue(data["success"])
        self.assertIn("updated", data)


@skipIfNoFastAPI
class TestAPIEndpointValidation(unittest.TestCase):
    """Tests for API request validation."""

    @classmethod
    def setUpClass(cls):
        """Set up test fixtures once for all tests."""
        from neuroshard import runner
        cls.runner = runner
        cls.client = TestClient(runner.node_app)

    def setUp(self):
        """Set up mocks before each test."""
        self.mock_node = MagicMock()
        self.mock_node.node_id = "test_node"
        self.mock_node.my_layer_ids = [0, 1]
        
        self.mock_p2p = MagicMock()
        self.mock_p2p.ledger = MagicMock()
        self.mock_p2p.peers = {}
        
        self.node_patcher = patch.object(self.runner, 'NEURO_NODE', self.mock_node)
        self.p2p_patcher = patch.object(self.runner, 'P2P', self.mock_p2p)
        
        self.node_patcher.start()
        self.p2p_patcher.start()

    def tearDown(self):
        """Clean up mocks after each test."""
        self.node_patcher.stop()
        self.p2p_patcher.stop()

    def test_inference_missing_prompt(self):
        """Test inference without prompt field."""
        response = self.client.post(
            "/api/v1/inference",
            json={
                "max_tokens": 50,
            }
        )
        
        # Should return validation error
        self.assertEqual(response.status_code, 422)

    def test_inference_invalid_temperature(self):
        """Test inference with invalid temperature."""
        self.mock_node.inference = MagicMock(return_value="text")
        
        response = self.client.post(
            "/api/v1/inference",
            json={
                "prompt": "test",
                "temperature": -1.0,  # Invalid: should be >= 0
            }
        )
        
        # May still work or return 422 depending on validation
        self.assertIn(response.status_code, [200, 422])

    def test_wallet_send_negative_amount(self):
        """Test sending negative amount."""
        response = self.client.post(
            "/api/v1/wallet/send",
            json={
                "to": "recipient",
                "amount": -100.0,
            }
        )
        
        # Should reject negative amounts
        self.assertIn(response.status_code, [400, 422])

    def test_stake_invalid_duration(self):
        """Test staking with invalid duration."""
        response = self.client.post(
            "/api/v1/wallet/stake",
            json={
                "amount": 100.0,
                "duration_days": -5,  # Invalid
            }
        )
        
        self.assertIn(response.status_code, [400, 422])


@skipIfNoFastAPI
class TestAPIResponseFormat(unittest.TestCase):
    """Tests for API response format consistency."""

    @classmethod
    def setUpClass(cls):
        """Set up test fixtures once for all tests."""
        from neuroshard import runner
        cls.runner = runner
        cls.client = TestClient(runner.node_app)

    def setUp(self):
        """Set up mocks before each test."""
        self.mock_node = MagicMock()
        self.mock_node.node_id = "test_node"
        self.mock_node.my_layer_ids = [0, 1]
        self.mock_node.get_stats.return_value = {}
        
        self.mock_p2p = MagicMock()
        self.mock_p2p.ledger = MagicMock()
        self.mock_p2p.ledger.get_balance.return_value = 1000.0
        self.mock_p2p.ledger.stake = 0
        self.mock_p2p.ledger.stake_multiplier = 1.0
        self.mock_p2p.ledger.pending_rewards = 0
        self.mock_p2p.peers = {}
        
        self.node_patcher = patch.object(self.runner, 'NEURO_NODE', self.mock_node)
        self.p2p_patcher = patch.object(self.runner, 'P2P', self.mock_p2p)
        self.start_patcher = patch.object(self.runner, 'START_TIME', 1733396400.0)
        
        self.node_patcher.start()
        self.p2p_patcher.start()
        self.start_patcher.start()

    def tearDown(self):
        """Clean up mocks after each test."""
        self.node_patcher.stop()
        self.p2p_patcher.stop()
        self.start_patcher.stop()

    def test_status_response_has_required_fields(self):
        """Test that status response has all required fields."""
        with patch('time.time', return_value=1733400000.0):
            with patch('psutil.cpu_percent', return_value=0):
                with patch('psutil.virtual_memory') as mock_mem:
                    mock_mem.return_value = MagicMock(used=0, total=1)
                    response = self.client.get("/api/v1/status")
        
        required_fields = [
            "node_id", "version", "uptime_seconds", "status",
            "role", "layers", "peer_count", "training", "resources"
        ]
        
        data = response.json()
        for field in required_fields:
            self.assertIn(field, data, f"Missing required field: {field}")

    def test_balance_response_structure(self):
        """Test that balance response has correct structure."""
        response = self.client.get("/api/v1/wallet/balance")
        
        data = response.json()
        self.assertIn("address", data)
        self.assertIn("balances", data)
        
        balances = data["balances"]
        self.assertIn("available", balances)
        self.assertIn("staked", balances)
        self.assertIn("pending", balances)
        self.assertIn("total", balances)

    def test_peers_response_structure(self):
        """Test that peers response has correct structure."""
        self.mock_p2p.peers = {
            "peer_1": {
                "node_id": "peer_1",
                "grpc_addr": "192.168.1.100:9000",
                "role": "worker",
                "layers": [0, 1],
                "last_seen": 1733400000.0,
            }
        }
        
        response = self.client.get("/api/v1/peers")
        
        data = response.json()
        self.assertIn("peers", data)
        self.assertIn("total", data)
        
        if data["peers"]:
            peer = data["peers"][0]
            self.assertIn("id", peer)
            self.assertIn("address", peer)
            self.assertIn("role", peer)
            self.assertIn("layers", peer)


if __name__ == "__main__":
    unittest.main()

