"""Tests for pulse-tradingview."""

import json
import threading
import time
import urllib.request
from unittest.mock import MagicMock, patch

import pytest

from pulse_tradingview import AlertConverter, SignalRouter, WebhookReceiver


# --- AlertConverter tests ---

class TestAlertConverter:
    def setup_method(self):
        self.converter = AlertConverter()

    # JSON format tests
    def test_parse_buy_json(self):
        raw = '{"action": "buy", "symbol": "BTCUSDT", "price": "83000", "interval": "1h"}'
        params = self.converter.parse(raw)
        assert params["direction"] == "long"
        assert params["pair"] == "BTC/USDT"
        assert params["price"] == 83000.0
        assert params["interval"] == "1h"
        assert params["source"] == "tradingview"

    def test_parse_sell_json(self):
        params = self.converter.parse('{"action": "sell", "symbol": "ETHUSDT"}')
        assert params["direction"] == "short"
        assert params["pair"] == "ETH/USDT"

    def test_parse_long_action(self):
        params = self.converter.parse('{"action": "long", "symbol": "SOLUSDT"}')
        assert params["direction"] == "long"

    def test_parse_short_action(self):
        params = self.converter.parse('{"action": "short", "symbol": "BTCUSDT"}')
        assert params["direction"] == "short"

    def test_parse_close_action(self):
        params = self.converter.parse('{"action": "close", "symbol": "BTCUSDT"}')
        assert params["direction"] == "neutral"

    def test_parse_confidence(self):
        params = self.converter.parse(
            '{"action": "buy", "symbol": "BTCUSDT", "confidence": "0.85"}'
        )
        assert params["confidence"] == 0.85

    def test_parse_confidence_percentage(self):
        # Some users send confidence as 85 instead of 0.85
        params = self.converter.parse(
            '{"action": "buy", "symbol": "BTCUSDT", "confidence": "85"}'
        )
        assert params["confidence"] == 0.85

    def test_default_confidence_is_1(self):
        params = self.converter.parse('{"action": "buy", "symbol": "BTCUSDT"}')
        assert params["confidence"] == 1.0

    def test_parse_ticker_field(self):
        # TradingView uses {{ticker}} which sends as "ticker" key
        params = self.converter.parse('{"action": "buy", "ticker": "BTCUSDT"}')
        assert params["pair"] == "BTC/USDT"

    def test_parse_with_extra_fields(self):
        raw = '{"action": "buy", "symbol": "BTCUSDT", "rsi": "28", "strategy": "RSI_BB"}'
        params = self.converter.parse(raw)
        assert params["direction"] == "long"
        assert "extra" in params
        assert params["extra"]["rsi"] == "28"

    # Symbol conversion tests
    def test_symbol_btcusdt(self):
        params = self.converter.parse('{"action": "buy", "symbol": "BTCUSDT"}')
        assert params["pair"] == "BTC/USDT"

    def test_symbol_ethbtc(self):
        params = self.converter.parse('{"action": "buy", "symbol": "ETHBTC"}')
        assert params["pair"] == "ETH/BTC"

    def test_symbol_solusdt(self):
        params = self.converter.parse('{"action": "sell", "symbol": "SOLUSDT"}')
        assert params["pair"] == "SOL/USDT"

    def test_symbol_already_formatted(self):
        params = self.converter.parse('{"action": "buy", "symbol": "BTC/USDT"}')
        assert params["pair"] == "BTC/USDT"

    def test_symbol_eur_pair(self):
        params = self.converter.parse('{"action": "buy", "symbol": "BTCEUR"}')
        assert params["pair"] == "BTC/EUR"

    # Interval tests
    def test_interval_60_to_1h(self):
        params = self.converter.parse('{"action": "buy", "symbol": "BTCUSDT", "interval": "60"}')
        assert params["interval"] == "1h"

    def test_interval_1D(self):
        params = self.converter.parse('{"action": "buy", "symbol": "BTCUSDT", "interval": "1D"}')
        assert params["interval"] == "1d"

    def test_interval_passthrough(self):
        params = self.converter.parse('{"action": "buy", "symbol": "BTCUSDT", "interval": "4h"}')
        assert params["interval"] == "4h"

    # Plain text format
    def test_parse_plain_buy(self):
        params = self.converter.parse("BUY BTCUSDT")
        assert params["direction"] == "long"
        assert params["pair"] == "BTC/USDT"

    def test_parse_plain_sell_with_price(self):
        params = self.converter.parse("SELL ETHUSDT 3500")
        assert params["direction"] == "short"
        assert params["price"] == 3500.0

    def test_parse_plain_case_insensitive(self):
        params = self.converter.parse("buy BTCUSDT")
        assert params["direction"] == "long"

    # Error cases
    def test_invalid_action(self):
        with pytest.raises(ValueError, match="Unknown action"):
            self.converter.parse('{"action": "hold", "symbol": "BTCUSDT"}')

    def test_missing_symbol(self):
        with pytest.raises(ValueError, match="missing 'symbol'"):
            self.converter.parse('{"action": "buy"}')

    def test_invalid_json(self):
        with pytest.raises(ValueError, match="Invalid JSON"):
            self.converter.parse('{"action": "buy", broken}')

    def test_plain_text_too_short(self):
        with pytest.raises(ValueError):
            self.converter.parse("BUY")


# --- SignalRouter tests ---

class TestSignalRouter:
    def test_add_adapter(self):
        router = SignalRouter()
        mock = MagicMock()
        router.add_adapter(mock, "test")
        assert len(router._adapters) == 1

    def test_route_calls_adapter(self):
        router = SignalRouter()
        mock_adapter = MagicMock()
        mock_adapter.send.return_value = MagicMock(content={"result": "ok"})

        with patch("pulse_tradingview.router.PulseMessage") if False else patch.object(
            router, "_send_to_adapter", return_value={"result": "ok"}
        ):
            router.add_adapter(mock_adapter, "test")
            results = router.route({
                "pair": "BTC/USDT",
                "symbol": "BTCUSDT",
                "direction": "long",
                "confidence": 0.9,
                "source": "tradingview",
            })

        assert len(results) == 1
        assert results[0]["status"] == "ok"

    def test_filter_blocks_low_confidence(self):
        router = SignalRouter()
        mock = MagicMock()
        router.add_adapter(mock, "test")
        router.add_filter(lambda p: p["confidence"] >= 0.8)

        results = router.route({
            "pair": "BTC/USDT",
            "direction": "long",
            "confidence": 0.5,
        })
        assert results == []

    def test_filter_allows_high_confidence(self):
        router = SignalRouter()
        mock = MagicMock()
        router.add_adapter(mock, "test")
        router.add_filter(lambda p: p["confidence"] >= 0.8)

        with patch.object(router, "_send_to_adapter", return_value={}):
            results = router.route({
                "pair": "BTC/USDT",
                "direction": "long",
                "confidence": 0.9,
            })
        assert len(results) == 1

    def test_pair_filter(self):
        router = SignalRouter()
        mock = MagicMock()
        router.add_adapter(mock, "test")
        router.add_filter(lambda p: p["pair"] in ["BTC/USDT", "ETH/USDT"])

        results = router.route({"pair": "DOGE/USDT", "direction": "long", "confidence": 1.0})
        assert results == []

    def test_no_adapters_returns_empty(self):
        router = SignalRouter()
        results = router.route({"pair": "BTC/USDT", "direction": "long"})
        assert results == []

    def test_adapter_error_is_caught(self):
        router = SignalRouter()
        mock = MagicMock()
        router.add_adapter(mock, "test")

        with patch.object(router, "_send_to_adapter", side_effect=RuntimeError("API error")):
            results = router.route({"pair": "BTC/USDT", "direction": "long", "confidence": 1.0})

        assert len(results) == 1
        assert results[0]["status"] == "error"
        assert "API error" in results[0]["error"]

    def test_chaining(self):
        router = SignalRouter()
        mock = MagicMock()
        result = router.add_adapter(mock).add_filter(lambda p: True)
        assert result is router


# --- WebhookReceiver tests ---

class TestWebhookReceiver:
    def test_start_stop(self):
        received = []
        receiver = WebhookReceiver(port=18888, on_signal=lambda p: received.append(p))
        receiver.start()
        time.sleep(0.1)
        receiver.stop()

    def test_health_check(self):
        receiver = WebhookReceiver(port=18889)
        receiver.start()
        time.sleep(0.1)

        try:
            req = urllib.request.Request("http://localhost:18889/")
            with urllib.request.urlopen(req, timeout=2) as r:
                data = json.loads(r.read())
            assert data["status"] == "ok"
            assert "pulse-tradingview" in data["service"]
        finally:
            receiver.stop()

    def test_receives_alert(self):
        received = []

        def handler(params):
            received.append(params)
            return []

        receiver = WebhookReceiver(port=18890, on_signal=handler)
        receiver.start()
        time.sleep(0.1)

        try:
            payload = json.dumps({
                "action": "buy",
                "symbol": "BTCUSDT",
                "price": "83000",
            }).encode()
            req = urllib.request.Request(
                "http://localhost:18890/",
                data=payload,
                headers={"Content-Type": "application/json"},
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=2) as r:
                response = json.loads(r.read())

            assert response["status"] == "ok"
            assert len(received) == 1
            assert received[0]["direction"] == "long"
            assert received[0]["pair"] == "BTC/USDT"
        finally:
            receiver.stop()

    def test_bad_payload_returns_400(self):
        receiver = WebhookReceiver(port=18891)
        receiver.start()
        time.sleep(0.1)

        try:
            payload = b'{"action": "hold", "symbol": "BTCUSDT"}'
            req = urllib.request.Request(
                "http://localhost:18891/",
                data=payload,
                headers={"Content-Type": "application/json"},
                method="POST",
            )
            try:
                urllib.request.urlopen(req, timeout=2)
                assert False, "Should have raised"
            except urllib.error.HTTPError as e:
                assert e.code == 400
        finally:
            receiver.stop()

    def test_context_manager(self):
        received = []
        with WebhookReceiver(port=18892, on_signal=lambda p: received.append(p)):
            time.sleep(0.05)
            # Server should be running
            req = urllib.request.Request("http://localhost:18892/")
            with urllib.request.urlopen(req, timeout=2) as r:
                data = json.loads(r.read())
            assert data["status"] == "ok"


# --- Integration test ---

class TestIntegration:
    def test_full_pipeline(self):
        """TradingView alert → WebhookReceiver → AlertConverter → SignalRouter"""
        routed = []

        converter = AlertConverter()
        router = SignalRouter()

        def fake_send(adapter, params, label):
            routed.append(params)
            return {"result": "ok"}

        mock_adapter = MagicMock()
        router.add_adapter(mock_adapter, "test")
        router._send_to_adapter = fake_send

        def on_signal(params):
            return router.route(params)

        receiver = WebhookReceiver(port=18893, on_signal=on_signal)
        receiver.start()
        time.sleep(0.1)

        try:
            alerts = [
                '{"action": "buy", "symbol": "BTCUSDT", "price": "83000", "confidence": "0.92"}',
                '{"action": "sell", "symbol": "ETHUSDT", "price": "3500"}',
            ]
            for alert in alerts:
                req = urllib.request.Request(
                    "http://localhost:18893/",
                    data=alert.encode(),
                    headers={"Content-Type": "application/json"},
                    method="POST",
                )
                with urllib.request.urlopen(req, timeout=2):
                    pass

            assert len(routed) == 2
            assert routed[0]["direction"] == "long"
            assert routed[0]["pair"] == "BTC/USDT"
            assert routed[0]["confidence"] == 0.92
            assert routed[1]["direction"] == "short"
            assert routed[1]["pair"] == "ETH/USDT"
        finally:
            receiver.stop()
