"""Tests for HorizonCloud client."""

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

from horizon.cloud import HorizonCloud, HorizonCloudError


# ── Initialization ───────────────────────────────────────────────────

def test_cloud_requires_api_key():
    with patch.dict(os.environ, {}, clear=True):
        os.environ.pop("HORIZON_API_KEY", None)
        with pytest.raises(HorizonCloudError, match="No API key"):
            HorizonCloud()


def test_cloud_rejects_invalid_prefix():
    with pytest.raises(HorizonCloudError, match="hz_sdk_"):
        HorizonCloud(api_key="bad_key_123")


def test_cloud_accepts_sdk_key():
    cloud = HorizonCloud(api_key="hz_sdk_" + "a" * 64)
    assert cloud._api_key.startswith("hz_sdk_")


def test_cloud_accepts_live_key():
    """hz_live_ keys still accepted for backward compatibility."""
    cloud = HorizonCloud(api_key="hz_live_" + "a" * 64)
    assert cloud._api_key.startswith("hz_live_")


def test_cloud_uses_env_var():
    with patch.dict(os.environ, {"HORIZON_API_KEY": "hz_sdk_" + "b" * 64}):
        cloud = HorizonCloud()
        assert "b" * 10 in cloud._api_key


def test_cloud_custom_base_url():
    cloud = HorizonCloud(api_key="hz_sdk_" + "c" * 64, base_url="https://custom.example.com/")
    assert cloud._base_url == "https://custom.example.com"  # trailing slash stripped


def test_cloud_repr():
    cloud = HorizonCloud(api_key="hz_sdk_" + "d" * 64)
    r = repr(cloud)
    assert "HorizonCloud" in r
    assert "hz_sdk_ddddd..." in r  # masked key (first 12 chars)


# ── URL construction ─────────────────────────────────────────────────

def test_url_construction():
    cloud = HorizonCloud(api_key="hz_sdk_" + "e" * 64)
    assert cloud._url("/strategies") == f"{cloud._base_url}/api/v1/strategies"


# ── API methods (mocked) ────────────────────────────────────────────

@pytest.fixture
def cloud():
    return HorizonCloud(api_key="hz_sdk_" + "f" * 64)


def _mock_response(json_data, status_code=200):
    resp = MagicMock()
    resp.ok = status_code < 400
    resp.status_code = status_code
    resp.json.return_value = json_data
    return resp


def test_list_strategies(cloud):
    data = [{"id": "s1", "name": "Test"}]
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": data})):
        result = cloud.list_strategies()
        assert result == data


def test_get_strategy(cloud):
    data = {"id": "s1", "name": "Test", "status": "validated"}
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": data})):
        result = cloud.get_strategy("s1")
        assert result["id"] == "s1"


def test_deploy(cloud):
    dep = {"id": "d1", "status": "starting"}
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": dep}, 201)):
        result = cloud.deploy("s1", credential_id="cred1", mode="paper")
        assert result["status"] == "starting"


def test_deploy_with_markets(cloud):
    dep = {"id": "d2", "status": "pending"}
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": dep}, 201)) as mock_req:
        cloud.deploy("s1", credential_id="cred1", markets=["market-a", "market-b"])
        call_kwargs = mock_req.call_args
        assert "market-a" in str(call_kwargs)


def test_stop(cloud):
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": {"success": True, "worker_reachable": True}})):
        result = cloud.stop("s1")
        assert result["success"] is True


def test_deployments(cloud):
    deps = [{"id": "d1", "status": "running"}, {"id": "d2", "status": "stopped"}]
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": deps})):
        result = cloud.deployments("s1")
        assert len(result) == 2


def test_status_active(cloud):
    deps = [{"id": "d1", "status": "running"}, {"id": "d2", "status": "stopped"}]
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": deps})):
        result = cloud.status("s1")
        assert result["status"] == "running"


def test_status_no_active(cloud):
    deps = [{"id": "d1", "status": "stopped"}]
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": deps})):
        result = cloud.status("s1")
        assert result["status"] == "stopped"


def test_status_no_deployments(cloud):
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": []})):
        result = cloud.status("s1")
        assert result["status"] == "no_deployments"


def test_metrics(cloud):
    data = {"latest": {"total_pnl": 100.0}, "history": []}
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": data})):
        result = cloud.metrics("s1")
        assert result["latest"]["total_pnl"] == 100.0


def test_logs(cloud):
    logs = [{"level": "info", "message": "started"}]
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": logs})):
        result = cloud.logs("s1")
        assert len(result) == 1


def test_logs_with_filters(cloud):
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": []})) as mock_req:
        cloud.logs("s1", level="error", limit=50)
        call_kwargs = mock_req.call_args
        assert "error" in str(call_kwargs)


def test_account(cloud):
    data = {"plan": "pro", "limits": {}, "usage": {}, "scopes": ["read", "deploy"]}
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": data})):
        result = cloud.account()
        assert result["plan"] == "pro"


# ── create_strategy ───────────────────────────────────────────────────

def test_create_strategy(cloud):
    data = {"id": "s1", "name": "MyMM", "class_name": "MyMMStrategy", "status": "validated"}
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": data}, 201)) as mock_req:
        result = cloud.create_strategy(name="MyMM", code="def pipeline(ctx):\n  return hz.quotes(fair=0.5)")
        assert result["status"] == "validated"
        assert result["id"] == "s1"
        # Verify POST was called with correct payload
        call_kwargs = mock_req.call_args
        assert "MyMM" in str(call_kwargs)


def test_create_strategy_validation_error(cloud):
    err_body = {
        "error": "Strategy code validation failed",
        "validation_errors": [{"line": 1, "message": "eval() is not allowed"}]
    }
    with patch.object(cloud._session, "request", return_value=_mock_response(err_body, 422)):
        with pytest.raises(HorizonCloudError, match="validation failed") as exc_info:
            cloud.create_strategy(name="Bad", code="eval('1+1')")
        assert exc_info.value.status_code == 422
        assert "validation_errors" in exc_info.value.body


def test_create_strategy_empty_name(cloud):
    with pytest.raises(HorizonCloudError, match="name is required"):
        cloud.create_strategy(name="", code="def pipeline(ctx): pass")


def test_create_strategy_empty_code(cloud):
    with pytest.raises(HorizonCloudError, match="code is required"):
        cloud.create_strategy(name="Test", code="")


def test_create_strategy_plan_limit(cloud):
    with patch.object(cloud._session, "request", return_value=_mock_response(
        {"error": "Strategy limit reached", "code": "PLAN_LIMIT", "plan": "free"}, 403
    )):
        with pytest.raises(HorizonCloudError, match="limit reached") as exc_info:
            cloud.create_strategy(name="TooMany", code="def p(ctx): pass")
        assert exc_info.value.status_code == 403


# ── validate ─────────────────────────────────────────────────────────

def test_validate_success(cloud):
    data = {"valid": True, "phase": "sandbox", "errors": []}
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": data})):
        result = cloud.validate("s1")
        assert result["valid"] is True
        assert result["phase"] == "sandbox"


def test_validate_failure(cloud):
    data = {"valid": False, "phase": "static", "errors": [{"line": 3, "message": "subprocess is not allowed"}]}
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": data})):
        result = cloud.validate("s1")
        assert result["valid"] is False
        assert len(result["errors"]) == 1


# ── credentials ──────────────────────────────────────────────────────

def test_save_credentials(cloud):
    data = {"id": "cred1", "label": "My Key", "exchange": "polymarket"}
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": data}, 201)):
        result = cloud.save_credentials(
            private_key="0x" + "ab" * 32,
            label="My Key",
            exchange="polymarket"
        )
        assert result["id"] == "cred1"
        assert result["exchange"] == "polymarket"


def test_save_credentials_without_prefix(cloud):
    data = {"id": "cred2", "label": "Default", "exchange": "polymarket"}
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": data}, 201)):
        result = cloud.save_credentials(private_key="ab" * 32)
        assert result["id"] == "cred2"


def test_save_credentials_invalid_key(cloud):
    with pytest.raises(HorizonCloudError, match="Invalid private key"):
        cloud.save_credentials(private_key="not-a-hex-key")


def test_save_credentials_short_key(cloud):
    with pytest.raises(HorizonCloudError, match="Invalid private key"):
        cloud.save_credentials(private_key="0x" + "ab" * 16)  # only 32 hex chars


def test_save_credentials_invalid_exchange(cloud):
    with pytest.raises(HorizonCloudError, match="polymarket.*kalshi"):
        cloud.save_credentials(private_key="ab" * 32, exchange="binance")


def test_list_credentials(cloud):
    creds = [{"id": "c1", "label": "Key1", "exchange": "polymarket"}]
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": creds})):
        result = cloud.list_credentials()
        assert len(result) == 1
        assert result[0]["label"] == "Key1"


def test_credentials_never_return_key(cloud):
    """Verify that the response structure never includes private key material."""
    creds = [{"id": "c1", "label": "Key1", "exchange": "polymarket"}]
    with patch.object(cloud._session, "request", return_value=_mock_response({"data": creds})):
        result = cloud.list_credentials()
        for cred in result:
            assert "private_key" not in cred
            assert "encrypted_private_key" not in cred
            assert "iv" not in cred
            assert "auth_tag" not in cred


# ── full lifecycle (mocked) ──────────────────────────────────────────

def test_full_lifecycle(cloud):
    """Test create → validate → deploy → status → stop flow."""
    strat = {"id": "s1", "name": "Test", "status": "validated"}
    cred = {"id": "c1", "label": "Key", "exchange": "polymarket"}
    valid = {"valid": True, "phase": "sandbox", "errors": []}
    dep = {"id": "d1", "status": "running"}
    deps = [dep]
    stop_resp = {"success": True, "worker_reachable": True}

    responses = [
        _mock_response({"data": strat}, 201),    # create_strategy
        _mock_response({"data": cred}, 201),      # save_credentials
        _mock_response({"data": valid}),           # validate
        _mock_response({"data": dep}, 201),        # deploy
        _mock_response({"data": deps}),            # deployments (for status)
        _mock_response({"data": stop_resp}),       # stop
    ]
    call_count = [0]
    def side_effect(*args, **kwargs):
        idx = call_count[0]
        call_count[0] += 1
        return responses[idx]

    with patch.object(cloud._session, "request", side_effect=side_effect):
        s = cloud.create_strategy(name="Test", code="def p(ctx): return hz.quotes(fair=0.5)")
        assert s["id"] == "s1"

        c = cloud.save_credentials(private_key="ab" * 32)
        assert c["id"] == "c1"

        v = cloud.validate("s1")
        assert v["valid"] is True

        d = cloud.deploy("s1", credential_id="c1")
        assert d["status"] == "running"

        st = cloud.status("s1")
        assert st["status"] == "running"

        r = cloud.stop("s1")
        assert r["success"] is True


# ── Error handling ───────────────────────────────────────────────────

def test_api_error(cloud):
    with patch.object(cloud._session, "request", return_value=_mock_response({"error": "Not found"}, 404)):
        with pytest.raises(HorizonCloudError, match="Not found") as exc_info:
            cloud.get_strategy("nonexistent")
        assert exc_info.value.status_code == 404


def test_rate_limit_error(cloud):
    with patch.object(cloud._session, "request", return_value=_mock_response({"error": "Rate limit exceeded"}, 429)):
        with pytest.raises(HorizonCloudError, match="Rate limit"):
            cloud.list_strategies()


def test_auth_error(cloud):
    with patch.object(cloud._session, "request", return_value=_mock_response({"error": "Invalid API key"}, 401)):
        with pytest.raises(HorizonCloudError) as exc_info:
            cloud.list_strategies()
        assert exc_info.value.status_code == 401


# ── wait_for_running ─────────────────────────────────────────────────

def test_wait_for_running_immediate(cloud):
    dep = {"id": "d1", "status": "running"}
    with patch.object(cloud, "status", return_value=dep):
        result = cloud.wait_for_running("s1", timeout=5)
        assert result["status"] == "running"


def test_wait_for_running_terminal(cloud):
    dep = {"id": "d1", "status": "error", "error_message": "crash"}
    with patch.object(cloud, "status", return_value=dep):
        with pytest.raises(HorizonCloudError, match="terminal status"):
            cloud.wait_for_running("s1", timeout=5)


def test_wait_for_running_timeout(cloud):
    dep = {"id": "d1", "status": "pending"}
    with patch.object(cloud, "status", return_value=dep):
        with pytest.raises(HorizonCloudError, match="Timed out"):
            cloud.wait_for_running("s1", timeout=0.1, poll_interval=0.05)


# ── Import test ──────────────────────────────────────────────────────

def test_import_from_horizon():
    from horizon import HorizonCloud, HorizonCloudError
    assert HorizonCloud is not None
    assert HorizonCloudError is not None
