# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# SPDX-License-Identifier: MIT

import pytest
from langchain_core.messages import (
    AIMessageChunk,
    ChatMessageChunk,
    FunctionMessageChunk,
    HumanMessageChunk,
    SystemMessageChunk,
    ToolMessageChunk,
)

from src.llms import llm as llm_module
from src.llms.providers import dashscope as dashscope_module
from src.llms.providers.dashscope import (
    ChatDashscope,
    _convert_chunk_to_generation_chunk,
    _convert_delta_to_message_chunk,
)


class DummyChatDashscope:
    def __init__(self, **kwargs):
        self.kwargs = kwargs


@pytest.fixture
def dashscope_conf():
    return {
        "BASIC_MODEL": {
            "api_key": "k",
            "base_url": "https://dashscope.aliyuncs.com/v1",
            "model": "qwen3-235b-a22b-instruct-2507",
        },
        "REASONING_MODEL": {
            "api_key": "rk",
            "base_url": "https://dashscope.aliyuncs.com/v1",
            "model": "qwen3-235b-a22b-thinking-2507",
        },
    }


def test_convert_delta_to_message_chunk_roles_and_extras():
    # Assistant with reasoning + tool calls
    delta = {
        "role": "assistant",
        "content": "Hello",
        "reasoning_content": "Think...",
        "tool_calls": [
            {
                "id": "call_1",
                "index": 0,
                "function": {"name": "lookup", "arguments": '{\\"q\\":\\"x\\"}'},
            }
        ],
    }
    msg = _convert_delta_to_message_chunk(delta, AIMessageChunk)
    assert isinstance(msg, AIMessageChunk)
    assert msg.content == "Hello"
    assert msg.additional_kwargs.get("reasoning_content") == "Think..."
    # tool_call_chunks should be present
    assert getattr(msg, "tool_call_chunks", None)

    # Human
    delta = {"role": "user", "content": "Hi"}
    msg = _convert_delta_to_message_chunk(delta, HumanMessageChunk)
    assert isinstance(msg, HumanMessageChunk)

    # System
    delta = {"role": "system", "content": "Rules"}
    msg = _convert_delta_to_message_chunk(delta, SystemMessageChunk)
    assert isinstance(msg, SystemMessageChunk)

    # Function
    delta = {"role": "function", "name": "f", "content": "{}"}
    msg = _convert_delta_to_message_chunk(delta, FunctionMessageChunk)
    assert isinstance(msg, FunctionMessageChunk)

    # Tool
    delta = {"role": "tool", "tool_call_id": "t1", "content": "ok"}
    msg = _convert_delta_to_message_chunk(delta, ToolMessageChunk)
    assert isinstance(msg, ToolMessageChunk)


def test_convert_chunk_to_generation_chunk_skip_and_usage():
    # Skips content.delta type
    assert (
        _convert_chunk_to_generation_chunk(
            {"type": "content.delta"}, AIMessageChunk, None
        )
        is None
    )

    # Proper chunk with usage and finish info
    chunk = {
        "choices": [
            {
                "delta": {"role": "assistant", "content": "Hi"},
                "finish_reason": "stop",
            }
        ],
        "model": "qwen3-235b-a22b-instruct-2507",
        "system_fingerprint": "fp",
        "usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3},
    }
    gen = _convert_chunk_to_generation_chunk(chunk, AIMessageChunk, None)
    assert gen is not None
    assert isinstance(gen.message, AIMessageChunk)
    assert gen.message.content == "Hi"
    # usage metadata should attach to AI message
    assert getattr(gen.message, "usage_metadata", None) is not None
    assert gen.generation_info.get("finish_reason") == "stop"
    assert gen.generation_info.get("model_name") == "qwen3-235b-a22b-instruct-2507"
    assert gen.generation_info.get("system_fingerprint") == "fp"


def test_llm_selects_dashscope_and_sets_enable_thinking(monkeypatch, dashscope_conf):
    # Use dummy class to capture kwargs on construction
    monkeypatch.setattr(llm_module, "ChatDashscope", DummyChatDashscope)

    # basic -> enable_thinking False
    inst = llm_module._create_llm_use_conf("basic", dashscope_conf)
    assert isinstance(inst, DummyChatDashscope)
    assert inst.kwargs["extra_body"]["enable_thinking"] is False
    assert inst.kwargs["base_url"].find("dashscope.") > 0

    # reasoning -> enable_thinking True
    inst2 = llm_module._create_llm_use_conf("reasoning", dashscope_conf)
    assert isinstance(inst2, DummyChatDashscope)
    assert inst2.kwargs["extra_body"]["enable_thinking"] is True


def test_llm_verify_ssl_false_adds_http_clients(monkeypatch, dashscope_conf):
    monkeypatch.setattr(llm_module, "ChatDashscope", DummyChatDashscope)
    # turn off ssl
    dashscope_conf = {**dashscope_conf}
    dashscope_conf["BASIC_MODEL"] = {
        **dashscope_conf["BASIC_MODEL"],
        "verify_ssl": False,
    }

    inst = llm_module._create_llm_use_conf("basic", dashscope_conf)
    assert "http_client" in inst.kwargs
    assert "http_async_client" in inst.kwargs


def test_convert_delta_to_message_chunk_developer_and_function_call_and_tool_calls():
    # developer role -> SystemMessageChunk with __openai_role__
    delta = {"role": "developer", "content": "dev rules"}
    msg = _convert_delta_to_message_chunk(delta, SystemMessageChunk)
    assert isinstance(msg, SystemMessageChunk)
    assert msg.additional_kwargs.get("__openai_role__") == "developer"

    # function_call name None -> empty string
    delta = {"role": "assistant", "function_call": {"name": None, "arguments": "{}"}}
    msg = _convert_delta_to_message_chunk(delta, AIMessageChunk)
    assert isinstance(msg, AIMessageChunk)
    assert msg.additional_kwargs["function_call"]["name"] == ""

    # tool_calls: one valid, one missing function -> should not crash and create one chunk
    delta = {
        "role": "assistant",
        "tool_calls": [
            {"id": "t1", "index": 0, "function": {"name": "f", "arguments": "{}"}},
            {"id": "t2", "index": 1},  # missing function key
        ],
    }
    msg = _convert_delta_to_message_chunk(delta, AIMessageChunk)
    assert isinstance(msg, AIMessageChunk)
    # tool_calls copied as-is
    assert msg.additional_kwargs["tool_calls"][0]["id"] == "t1"
    # tool_call_chunks only for valid one
    assert getattr(msg, "tool_call_chunks") and len(msg.tool_call_chunks) == 1


def test_convert_delta_to_message_chunk_default_class_and_unknown_role():
    # No role, default human -> HumanMessageChunk
    delta = {"content": "hey"}
    msg = _convert_delta_to_message_chunk(delta, HumanMessageChunk)
    assert isinstance(msg, HumanMessageChunk)

    # Unknown role -> ChatMessageChunk with that role
    delta = {"role": "observer", "content": "hmm"}
    msg = _convert_delta_to_message_chunk(delta, ChatMessageChunk)
    assert isinstance(msg, ChatMessageChunk)
    assert msg.role == "observer"


def test_convert_chunk_to_generation_chunk_empty_choices_and_usage():
    chunk = {
        "choices": [],
        "usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3},
    }
    gen = _convert_chunk_to_generation_chunk(chunk, AIMessageChunk, None)
    assert gen is not None
    assert isinstance(gen.message, AIMessageChunk)
    assert gen.message.content == ""
    assert getattr(gen.message, "usage_metadata", None) is not None
    assert gen.generation_info is None


def test_convert_chunk_to_generation_chunk_includes_base_info_and_logprobs():
    chunk = {
        "choices": [
            {
                "delta": {"role": "assistant", "content": "T"},
                "logprobs": {"content": [{"token": "T", "logprob": -0.1}]},
            }
        ]
    }
    base_info = {"headers": {"a": "b"}}
    gen = _convert_chunk_to_generation_chunk(chunk, AIMessageChunk, base_info)
    assert gen is not None
    assert gen.message.content == "T"
    assert gen.generation_info.get("headers") == {"a": "b"}
    assert "logprobs" in gen.generation_info


def test_convert_chunk_to_generation_chunk_beta_stream_format():
    chunk = {
        "chunk": {
            "choices": [
                {"delta": {"role": "assistant", "content": "From beta stream format"}}
            ]
        }
    }
    gen = _convert_chunk_to_generation_chunk(chunk, AIMessageChunk, None)
    assert gen is not None
    assert gen.message.content == "From beta stream format"


def test_chatdashscope_create_chat_result_adds_reasoning_content(monkeypatch):
    # Dummy objects for the super() return
    class DummyMsg:
        def __init__(self):
            self.additional_kwargs = {}

    class DummyGen:
        def __init__(self):
            self.message = DummyMsg()

    class DummyChatResult:
        def __init__(self):
            self.generations = [DummyGen()]

    # Patch super()._create_chat_result to return our dummy structure
    def fake_super_create(self, response, generation_info=None):
        return DummyChatResult()

    monkeypatch.setattr(
        dashscope_module.ChatOpenAI, "_create_chat_result", fake_super_create
    )

    # Patch openai.BaseModel in the module under test
    class DummyBaseModel:
        pass

    monkeypatch.setattr(dashscope_module.openai, "BaseModel", DummyBaseModel)

    # Build a fake OpenAI-like response with reasoning_content
    class RMsg:
        def __init__(self, rc):
            self.reasoning_content = rc

    class Choice:
        def __init__(self, rc):
            self.message = RMsg(rc)

    class FakeResponse(DummyBaseModel):
        def __init__(self):
            self.choices = [Choice("Reasoning...")]

    llm = ChatDashscope(model="dummy", api_key="k")
    result = llm._create_chat_result(FakeResponse())
    assert (
        result.generations[0].message.additional_kwargs.get("reasoning_content")
        == "Reasoning..."
    )


def test_chatdashscope_create_chat_result_dict_passthrough(monkeypatch):
    class DummyMsg:
        def __init__(self):
            self.additional_kwargs = {}

    class DummyGen:
        def __init__(self):
            self.message = DummyMsg()

    class DummyChatResult:
        def __init__(self):
            self.generations = [DummyGen()]

    def fake_super_create(self, response, generation_info=None):
        return DummyChatResult()

    monkeypatch.setattr(
        dashscope_module.ChatOpenAI, "_create_chat_result", fake_super_create
    )

    llm = ChatDashscope(model="dummy", api_key="k")
    result = llm._create_chat_result({"raw": "dict"})
    # Should not inject reasoning_content for dict responses
    assert "reasoning_content" not in result.generations[0].message.additional_kwargs
