import pytest
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from app.main import app
from app.core.database import get_db, Base
from app.services.currency_service import currency_service

# Test database setup
SQLALCHEMY_DATABASE_URL = "sqlite:///./test_currency.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def override_get_db():
    try:
        db = TestingSessionLocal()
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)

def setup_database():
    """Setup test database"""
    Base.metadata.create_all(bind=engine)

def cleanup_database():
    """Clean up test database"""
    Base.metadata.drop_all(bind=engine)

class TestCurrencyService:
    """Test suite for Currency Service"""

    def setup_method(self):
        """Setup for each test method"""
        # Clear cache before each test
        currency_service.cache = {}
        currency_service.cache_expiry = {}

    def test_same_currency_conversion(self):
        """Test conversion between same currency returns 1.0"""
        rate = currency_service.get_exchange_rate("USD", "USD")
        assert rate == 1.0

        rate = currency_service.get_exchange_rate("EUR", "EUR")
        assert rate == 1.0

    def test_fallback_rates(self):
        """Test fallback exchange rates are used when API fails"""
        # Test USD to EUR conversion using fallback rates
        rate = currency_service.get_exchange_rate("USD", "EUR")
        expected_rate = currency_service.fallback_rates["USD"] / currency_service.fallback_rates["EUR"]
        assert rate == expected_rate

    @patch('app.services.currency_service.requests.get')
    def test_api_rate_fetching(self, mock_get):
        """Test successful API rate fetching"""
        # Mock successful API response
        mock_response = MagicMock()
        mock_response.json.return_value = {
            "conversion_rates": {
                "EUR": 0.85,
                "GBP": 0.73
            }
        }
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response

        # Test USD to EUR conversion
        rate = currency_service.get_exchange_rate("USD", "EUR")
        assert rate == 0.85

        # Test USD to GBP conversion
        rate = currency_service.get_exchange_rate("USD", "GBP")
        assert rate == 0.73

    @patch('app.services.currency_service.requests.get')
    def test_api_failure_fallback(self, mock_get):
        """Test fallback to static rates when API fails"""
        # Mock API failure
        mock_get.side_effect = Exception("API Error")

        # Test conversion falls back to static rates
        rate = currency_service.get_exchange_rate("USD", "EUR")
        expected_rate = currency_service.fallback_rates["USD"] / currency_service.fallback_rates["EUR"]
        assert rate == expected_rate

    def test_caching_mechanism(self):
        """Test that exchange rates are cached"""
        # Mock the API to track calls
        with patch('app.services.currency_service.requests.get') as mock_get:
            mock_response = MagicMock()
            mock_response.json.return_value = {"conversion_rates": {"EUR": 0.85}}
            mock_response.raise_for_status.return_value = None
            mock_get.return_value = mock_response

            # First call should hit API
            rate1 = currency_service.get_exchange_rate("USD", "EUR")
            assert mock_get.call_count == 1
            assert rate1 == 0.85

            # Second call should use cache
            rate2 = currency_service.get_exchange_rate("USD", "EUR")
            assert mock_get.call_count == 1  # Should not increase
            assert rate2 == 0.85

    def test_reverse_rate_calculation(self):
        """Test calculation of reverse rates from cached rates"""
        # Mock API to cache USD to EUR rate
        with patch('app.services.currency_service.requests.get') as mock_get:
            mock_response = MagicMock()
            mock_response.json.return_value = {"conversion_rates": {"EUR": 0.85}}
            mock_response.raise_for_status.return_value = None
            mock_get.return_value = mock_response

            # Cache USD to EUR rate
            currency_service.get_exchange_rate("USD", "EUR")

            # Clear the mock to test reverse calculation
            mock_get.reset_mock()

            # Test EUR to USD uses reverse calculation
            rate = currency_service.get_exchange_rate("EUR", "USD")
            assert mock_get.call_count == 0  # Should not hit API
            assert rate == 1.0 / 0.85  # Reverse of cached rate

    def test_amount_conversion(self):
        """Test amount conversion between currencies"""
        # Test with USD to EUR at 0.85 rate
        with patch.object(currency_service, 'get_exchange_rate', return_value=0.85):
            converted_amount = currency_service.convert_amount(100.0, "USD", "EUR")
            assert converted_amount == 85.0

        # Test with zero amount
        converted_amount = currency_service.convert_amount(0.0, "USD", "EUR")
        assert converted_amount == 0.0

        # Test with negative amount
        converted_amount = currency_service.convert_amount(-50.0, "USD", "EUR")
        assert converted_amount == 0.0

    def test_currency_formatting(self):
        """Test currency formatting with symbols"""
        # Test USD formatting
        formatted = currency_service.format_currency(123.45, "USD")
        assert formatted == "$123.45"

        # Test EUR formatting
        formatted = currency_service.format_currency(99.99, "EUR")
        assert formatted == "€99.99"

        # Test JPY formatting (no decimal places)
        formatted = currency_service.format_currency(1234, "JPY")
        assert formatted == "¥1234"

        # Test CNY formatting (no decimal places)
        formatted = currency_service.format_currency(5678, "CNY")
        assert formatted == "¥5678"

    def test_supported_currencies(self):
        """Test getting supported currencies list"""
        currencies = currency_service.get_supported_currencies()
        assert "USD" in currencies
        assert "EUR" in currencies
        assert "GBP" in currencies
        assert "JPY" in currencies
        assert currencies["USD"] == "US Dollar"
        assert currencies["EUR"] == "Euro"

class TestCurrencyAPI:
    """Test suite for Currency API endpoints"""

    @pytest.fixture(autouse=True)
    def setup(self):
        """Setup for each test"""
        setup_database()
        yield
        cleanup_database()

    @patch('app.services.currency_service.requests.get')
    def test_currency_conversion_endpoint(self, mock_get):
        """Test currency conversion API endpoint"""
        # Mock API response
        mock_response = MagicMock()
        mock_response.json.return_value = {
            "conversion_rates": {
                "EUR": 0.85,
                "GBP": 0.73
            }
        }
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response

        # Test conversion endpoint
        response = client.get("/api/currency/convert?amount=100&from_currency=USD&to_currency=EUR")
        assert response.status_code == 200

        data = response.json()
        assert data["amount"] == 100.0
        assert data["from_currency"] == "USD"
        assert data["to_currency"] == "EUR"
        assert data["exchange_rate"] == 0.85
        assert data["converted_amount"] == 85.0

    @patch('app.services.currency_service.requests.get')
    def test_batch_conversion_endpoint(self, mock_get):
        """Test batch currency conversion API endpoint"""
        # Mock API response
        mock_response = MagicMock()
        mock_response.json.return_value = {
            "conversion_rates": {
                "EUR": 0.85,
                "GBP": 0.73
            }
        }
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response

        # Test batch conversion
        conversion_data = {
            "amount": 100.0,
            "from_currency": "USD",
            "to_currencies": ["EUR", "GBP", "JPY"]
        }

        response = client.post("/api/currency/convert/batch", json=conversion_data)
        assert response.status_code == 200

        data = response.json()
        assert data["amount"] == 100.0
        assert data["from_currency"] == "USD"
        assert len(data["conversions"]) == 3

        # Check individual conversions
        conversions = {conv["currency"]: conv["converted_amount"] for conv in data["conversions"]}
        assert conversions["EUR"] == 85.0
        assert conversions["GBP"] == 73.0
        # JPY should use fallback rate
        expected_jpy = currency_service.fallback_rates["USD"] / currency_service.fallback_rates["JPY"] * 100
        assert conversions["JPY"] == expected_jpy

    def test_supported_currencies_endpoint(self):
        """Test supported currencies API endpoint"""
        response = client.get("/api/currency/supported")
        assert response.status_code == 200

        data = response.json()
        assert "currencies" in data
        currencies = data["currencies"]
        assert "USD" in currencies
        assert "EUR" in currencies
        assert currencies["USD"] == "US Dollar"

    def test_currency_formatting_endpoint(self):
        """Test currency formatting API endpoint"""
        response = client.get("/api/currency/format?amount=123.45&currency_code=USD")
        assert response.status_code == 200

        data = response.json()
        assert data["amount"] == 123.45
        assert data["currency_code"] == "USD"
        assert data["formatted_amount"] == "$123.45"

    def test_conversion_validation(self):
        """Test conversion endpoint validation"""
        # Test missing parameters
        response = client.get("/api/currency/convert?amount=100&from_currency=USD")
        assert response.status_code == 422

        # Test invalid amount
        response = client.get("/api/currency/convert?amount=-50&from_currency=USD&to_currency=EUR")
        assert response.status_code == 400

        # Test invalid currency codes
        response = client.get("/api/currency/convert?amount=100&from_currency=INVALID&to_currency=EUR")
        assert response.status_code == 400

if __name__ == "__main__":
    pytest.main([__file__, "-v"])