"""
Unit tests for visibe/client.py — type detection functions and Visibe client.
No framework dependencies required.
"""
import types
import pytest

from visibe.client import (
    _is_openai_client,
    _is_langchain_runnable,
    _is_crewai_crew,
    _is_autogen_model_client,
    _is_bedrock_client,
    Visibe,
)


# ------------------------------------------------------------------
# Helper: minimal mock objects with just the right attributes
# ------------------------------------------------------------------

def openai_like():
    return types.SimpleNamespace(
        chat=types.SimpleNamespace(
            completions=types.SimpleNamespace(create=lambda: None)
        )
    )


def langchain_like():
    return types.SimpleNamespace(invoke=lambda x: x, get_graph=lambda: None)


def crewai_like():
    return types.SimpleNamespace(kickoff=lambda: None, agents=[], tasks=[])


def autogen_like():
    return types.SimpleNamespace(
        create=lambda: None,
        create_stream=lambda: None,
        model_info={"family": "gpt-4o"},
    )


def bedrock_like_via_metadata():
    service_model = types.SimpleNamespace(service_name="bedrock-runtime")
    meta = types.SimpleNamespace(service_model=service_model)
    return types.SimpleNamespace(meta=meta)


def bedrock_like_via_duck():
    return types.SimpleNamespace(
        converse=lambda: None,
        converse_stream=lambda: None,
        invoke_model=lambda: None,
        meta=types.SimpleNamespace(),
    )


# ------------------------------------------------------------------
# _is_openai_client
# ------------------------------------------------------------------

def test_is_openai_client_positive():
    assert _is_openai_client(openai_like()) is True


def test_is_openai_client_rejects_string():
    assert _is_openai_client("not a client") is False


def test_is_openai_client_rejects_none():
    assert _is_openai_client(None) is False


def test_is_openai_client_rejects_bedrock():
    assert _is_openai_client(bedrock_like_via_duck()) is False


def test_is_openai_client_rejects_missing_completions():
    obj = types.SimpleNamespace(chat=types.SimpleNamespace())  # no .completions
    assert _is_openai_client(obj) is False


# ------------------------------------------------------------------
# _is_langchain_runnable
# ------------------------------------------------------------------

def test_is_langchain_runnable_positive():
    assert _is_langchain_runnable(langchain_like()) is True


def test_is_langchain_runnable_rejects_string():
    assert _is_langchain_runnable("chain") is False


def test_is_langchain_runnable_rejects_invoke_only():
    obj = types.SimpleNamespace(invoke=lambda x: x)  # missing get_graph
    assert _is_langchain_runnable(obj) is False


def test_is_langchain_runnable_rejects_none():
    assert _is_langchain_runnable(None) is False


# ------------------------------------------------------------------
# _is_crewai_crew
# ------------------------------------------------------------------

def test_is_crewai_crew_positive():
    assert _is_crewai_crew(crewai_like()) is True


def test_is_crewai_crew_rejects_missing_tasks():
    obj = types.SimpleNamespace(kickoff=lambda: None, agents=[])
    assert _is_crewai_crew(obj) is False


def test_is_crewai_crew_rejects_none():
    assert _is_crewai_crew(None) is False


def test_is_crewai_crew_rejects_openai():
    assert _is_crewai_crew(openai_like()) is False


# ------------------------------------------------------------------
# _is_autogen_model_client
# ------------------------------------------------------------------

def test_is_autogen_model_client_positive():
    assert _is_autogen_model_client(autogen_like()) is True


def test_is_autogen_model_client_rejects_missing_model_info():
    obj = types.SimpleNamespace(create=lambda: None, create_stream=lambda: None)
    assert _is_autogen_model_client(obj) is False


def test_is_autogen_model_client_rejects_none():
    assert _is_autogen_model_client(None) is False


# ------------------------------------------------------------------
# _is_bedrock_client
# ------------------------------------------------------------------

def test_is_bedrock_client_via_service_metadata():
    assert _is_bedrock_client(bedrock_like_via_metadata()) is True


def test_is_bedrock_client_wrong_service_name():
    service_model = types.SimpleNamespace(service_name="s3")
    meta = types.SimpleNamespace(service_model=service_model)
    obj = types.SimpleNamespace(meta=meta)
    assert _is_bedrock_client(obj) is False


def test_is_bedrock_client_via_duck_typing():
    assert _is_bedrock_client(bedrock_like_via_duck()) is True


def test_is_bedrock_client_rejects_string():
    assert _is_bedrock_client("bedrock") is False


def test_is_bedrock_client_rejects_none():
    assert _is_bedrock_client(None) is False


def test_is_bedrock_client_rejects_openai():
    assert _is_bedrock_client(openai_like()) is False


# ------------------------------------------------------------------
# Visibe client — init validation
# ------------------------------------------------------------------

def test_visibe_invalid_timeout_raises():
    with pytest.raises(ValueError, match="timeout"):
        Visibe(api_key="sk_live_test", timeout=0)


def test_visibe_negative_timeout_raises():
    with pytest.raises(ValueError, match="timeout"):
        Visibe(api_key="sk_live_test", timeout=-1)


def test_visibe_invalid_api_url_raises():
    with pytest.raises(ValueError, match="http"):
        Visibe(api_key="sk_live_test", api_url="localhost:3000")


def test_visibe_trailing_slash_stripped():
    obs = Visibe(api_key="sk_live_abc", api_url="http://localhost:3000/")
    assert not obs.api_url.endswith("/")


def test_visibe_session_id_stored():
    obs = Visibe(api_key="sk_live_abc", api_url="http://localhost:3000", session_id="user-42")
    assert obs.session_id == "user-42"


def test_visibe_api_key_stored():
    obs = Visibe(api_key="sk_live_abc123", api_url="http://localhost:3000")
    assert obs.api_key == "sk_live_abc123"


# ------------------------------------------------------------------
# Visibe.create_trace — session_id injection
# ------------------------------------------------------------------

def test_create_trace_injects_session_id(monkeypatch):
    obs = Visibe(api_key="sk_live_abc", api_url="http://localhost:3000", session_id="sess-99")

    captured = {}
    monkeypatch.setattr(obs.api_client, "create_trace", lambda data: captured.update(data) or True)

    obs.create_trace({"trace_id": "t1", "name": "test"})
    assert captured.get("session_id") == "sess-99"


def test_create_trace_does_not_inject_session_id_if_already_set(monkeypatch):
    obs = Visibe(api_key="sk_live_abc", api_url="http://localhost:3000", session_id="sess-99")

    captured = {}
    monkeypatch.setattr(obs.api_client, "create_trace", lambda data: captured.update(data) or True)

    obs.create_trace({"trace_id": "t1", "session_id": "custom-session"})
    assert captured["session_id"] == "custom-session"


def test_create_trace_no_session_id_when_not_set(monkeypatch):
    obs = Visibe(api_key="sk_live_abc", api_url="http://localhost:3000")

    captured = {}
    monkeypatch.setattr(obs.api_client, "create_trace", lambda data: captured.update(data) or True)

    obs.create_trace({"trace_id": "t1"})
    assert "session_id" not in captured


# ------------------------------------------------------------------
# Visibe.instrument — unsupported type raises TypeError
# ------------------------------------------------------------------

def test_instrument_unsupported_type_raises():
    obs = Visibe(api_key="sk_live_abc", api_url="http://localhost:3000")
    with pytest.raises(TypeError, match="Unsupported"):
        obs.instrument("not a valid client")


def test_track_none_client_raises():
    obs = Visibe(api_key="sk_live_abc", api_url="http://localhost:3000")
    with pytest.raises(TypeError):
        obs.track(None)


def test_track_unsupported_type_raises():
    obs = Visibe(api_key="sk_live_abc", api_url="http://localhost:3000")
    with pytest.raises(TypeError, match="Unsupported"):
        obs.track("not a client")
