import os
import sys


import pytest

sys.path.insert(
    0, os.path.abspath("../../..")
)  # Adds the parent directory to the system path

from unittest.mock import MagicMock, patch
from litellm.integrations.gitlab.gitlab_prompt_manager import GitLabPromptManager


# -----------------------------
# Basic init & template loading
# -----------------------------
@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_manager_initialization_with_root_folder(mock_client_class):
    """Loads a prompt from the repo root when no prompts_path is specified."""
    mock_client = MagicMock()
    mock_client.get_file_content.return_value = """---
model: gpt-4
temperature: 0.7
max_tokens: 150
---
System: You are a helpful assistant.

User: {{user_message}}"""
    mock_client_class.return_value = mock_client

    config = {
        "project": "group/sub/repo",
        "access_token": "glpat_xxx",
        # no prompts_path -> root
    }

    manager = GitLabPromptManager(config, prompt_id="test_prompt")
    # Should have loaded the prompt
    assert "test_prompt" in manager.prompt_manager.prompts
    template = manager.prompt_manager.prompts["test_prompt"]
    assert template.model == "gpt-4"
    assert template.temperature == 0.7
    assert template.max_tokens == 150

    # Ensures correct file path was requested at repo root (test_prompt.prompt)
    mock_client.get_file_content.assert_called_with("test_prompt.prompt", ref=None)

    # Rendering
    rendered = manager.prompt_manager.render_template(
        "test_prompt", {"user_message": "What is AI?"}
    )
    assert "You are a helpful assistant." in rendered
    assert "What is AI?" in rendered


@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_manager_with_prompts_path(mock_client_class):
    """Loads a prompt from a configured prompts folder; ID maps to folder + .prompt."""
    mock_client = MagicMock()
    mock_client.get_file_content.return_value = "Hello {{name}}!"
    mock_client_class.return_value = mock_client

    config = {
        "project": "group/repo",
        "access_token": "token",
        "prompts_path": "prompts/chat",  # folder setting
    }

    manager = GitLabPromptManager(config, prompt_id="greet/hi")
    # Expected path: prompts/chat/greet/hi.prompt
    mock_client.get_file_content.assert_called_with(
        "prompts/chat/greet/hi.prompt", ref=None
    )

    rendered = manager.prompt_manager.render_template("greet/hi", {"name": "World"})
    assert rendered == "Hello World!"


# -----------------------------
# Error handling / validation
# -----------------------------
@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_manager_error_handling_load(mock_client_class):
    """Errors from GitLabClient surface with helpful context."""
    mock_client = MagicMock()
    mock_client.get_file_content.side_effect = Exception("GitLab API error")
    mock_client_class.return_value = mock_client

    config = {"project": "g/s/r", "access_token": "tkn"}

    with pytest.raises(
        Exception, match="Failed to load prompt 'gitlab::oops' from GitLab"
    ):
        GitLabPromptManager(config, prompt_id="oops").prompt_manager


def test_gitlab_prompt_manager_config_validation_via_client_ctor():
    """
    If GitLabClient validates config in __init__, simulate that with a side_effect.
    Ensures manager surfaces the ValueError while building prompt_manager.
    """
    with patch(
        "litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient",
        side_effect=ValueError("project and access_token are required"),
    ):
        with pytest.raises(ValueError, match="project and access_token are required"):
            GitLabPromptManager({}).prompt_manager


# -----------------------------
# Message parsing
# -----------------------------
@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_manager_message_parsing(mock_client_class):
    mock_client = MagicMock()
    mock_client.get_file_content.return_value = """---
model: gpt-4
---
System: You are a helpful assistant.

User: {{user_message}}

Assistant: I'll help you with that."""
    mock_client_class.return_value = mock_client

    config = {"project": "g/s/r", "access_token": "t"}

    manager = GitLabPromptManager(config, prompt_id="conversation_prompt")

    messages = manager._parse_prompt_to_messages(
        "System: You are a helpful assistant.\n\nUser: Hello!\n\nAssistant: Hi there!"
    )
    assert len(messages) == 3
    assert messages[0]["role"] == "system"
    assert messages[0]["content"] == "You are a helpful assistant."
    assert messages[1]["role"] == "user"
    assert messages[1]["content"] == "Hello!"
    assert messages[2]["role"] == "assistant"
    assert messages[2]["content"] == "Hi there!"


# -----------------------------
# pre_call_hook behavior & ref precedence
# -----------------------------
@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_manager_pre_call_hook_updates_params(mock_client_class):
    mock_client = MagicMock()
    mock_client.get_file_content.return_value = """---
model: gpt-4o
temperature: 0.8
max_tokens: 256
---
System: You are a helpful assistant.

User: {{user_message}}"""
    mock_client_class.return_value = mock_client

    config = {"project": "g/s/r", "access_token": "tkn"}

    manager = GitLabPromptManager(config, prompt_id="test_prompt")

    original_messages = [{"role": "user", "content": "This will be ignored"}]
    litellm_params = {"api_key": "keep-me"}

    result_messages, result_params = manager.pre_call_hook(
        user_id="u",
        messages=original_messages,
        litellm_params=litellm_params,
        prompt_id="test_prompt",
        prompt_variables={"user_message": "What is AI?"},
    )

    # Prompt parsed into messages
    assert len(result_messages) == 2
    assert result_messages[0]["role"] == "system"
    assert result_messages[1]["role"] == "user"
    assert result_messages[1]["content"] == "What is AI?"

    # Params merged + preserved
    assert result_params["model"] == "gpt-4o"
    assert result_params["temperature"] == 0.8
    assert result_params["max_tokens"] == 256
    assert result_params["api_key"] == "keep-me"


@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_manager_pre_call_hook_ref_precedence(mock_client_class):
    """
    Precedence for selecting git ref:
      prompt_version (arg) > git_ref kwarg > manager's _ref_override > client's default
    Validate that the chosen ref gets passed down to client.get_file_content.
    """
    mock_client = MagicMock()

    # Return any minimal valid prompt; we just need the call path to succeed.
    mock_client.get_file_content.return_value = """---
model: gpt-4
---
User: {{q}}"""
    mock_client_class.return_value = mock_client

    config = {"project": "g/s/r", "access_token": "tkn"}

    # Set a manager-level default ref override
    manager = GitLabPromptManager(config, prompt_id=None, ref="manager-default")

    # 1) No prior load; call with prompt_version -> should win
    _msgs, _params = manager.pre_call_hook(
        user_id="u",
        messages=[],
        litellm_params={},
        prompt_id="p1",
        prompt_variables={"q": "hello"},
        prompt_version="explicit-sha",
    )
    # get_file_content called with ref="explicit-sha"
    mock_client.get_file_content.assert_any_call("p1.prompt", ref="explicit-sha")

    # 2) Use git_ref kwarg (when no prompt_version)
    _msgs, _params = manager.pre_call_hook(
        user_id="u",
        messages=[],
        litellm_params={},
        prompt_id="p2",
        prompt_variables={"q": "hello"},
        git_ref="per-call-branch",
    )
    mock_client.get_file_content.assert_any_call("p2.prompt", ref="per-call-branch")

    # 3) Neither prompt_version nor git_ref -> falls back to manager _ref_override
    _msgs, _params = manager.pre_call_hook(
        user_id="u",
        messages=[],
        litellm_params={},
        prompt_id="p3",
        prompt_variables={"q": "hello"},
    )
    mock_client.get_file_content.assert_any_call("p3.prompt", ref="manager-default")


# -----------------------------
# Listing & availability
# -----------------------------
@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_manager_list_templates_with_prompts_path(mock_client_class):
    mock_client = MagicMock()
    mock_client.list_files.return_value = [
        "prompts/chat/a.prompt",
        "prompts/chat/sub/b.prompt",
        "prompts/chat/ignore.txt",
    ]
    mock_client.get_file_content.return_value = "Hello"
    mock_client_class.return_value = mock_client

    config = {
        "project": "g/s/r",
        "access_token": "tkn",
        "prompts_path": "prompts/chat",
    }

    manager = GitLabPromptManager(config, prompt_id="a")

    # list_templates strips folder prefix + extension
    ids = manager.get_available_prompts()
    assert "a" in ids
    assert "gitlab::sub::b" in ids
    assert all(not x.endswith(".prompt") for x in ids)
    assert all("/prompts/chat/" not in x for x in ids)


@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_template_manager_load_all_prompts(mock_client_class):
    """load_all_prompts should fetch all .prompt files and populate the internal cache."""
    mock_client = MagicMock()
    mock_client.list_files.return_value = [
        "prompts/a.prompt",
        "prompts/sub/b.prompt",
    ]
    mock_client.get_file_content.side_effect = [
        "Hello {{x}}",  # for a.prompt
        "---\nmodel: gpt-4\n---\nUser: {{y}}",  # for b.prompt with frontmatter
    ]
    mock_client_class.return_value = mock_client

    config = {
        "project": "g/s/r",
        "access_token": "tkn",
        "prompts_path": "prompts",
    }

    pm = GitLabPromptManager(config).prompt_manager
    loaded = pm.load_all_prompts()
    assert set(loaded) == {"gitlab::a", "gitlab::sub::b"}
    assert "gitlab::a" in pm.prompts and "gitlab::sub::b" in pm.prompts


# -----------------------------
# post_call & integration name
# -----------------------------
def test_gitlab_prompt_manager_integration_name():
    config = {"project": "g/s/r", "access_token": "tkn"}
    manager = GitLabPromptManager(config)
    assert manager.integration_name == "gitlab"


@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_manager_post_call_hook_passthrough(mock_client_class):
    mock_client = MagicMock()
    mock_client.get_file_content.return_value = "User: {{m}}"
    mock_client_class.return_value = mock_client

    config = {"project": "g/s/r", "access_token": "tkn"}

    manager = GitLabPromptManager(config, prompt_id="p")

    dummy_response = MagicMock()
    out = manager.post_call_hook(
        user_id="u",
        response=dummy_response,
        input_messages=[{"role": "user", "content": "x"}],
        litellm_params={},
        prompt_id="p",
    )
    assert out is dummy_response


@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_version_precedence_prompt_version_wins(mock_client_class):
    """
    prompt_version > git_ref kwarg > manager _ref_override.
    Ensure prompt_version wins and is passed down to GitLabClient.get_file_content.
    """
    mock_client = MagicMock()
    mock_client.get_file_content.return_value = """---
model: gpt-4
---
User: {{q}}"""
    mock_client_class.return_value = mock_client

    cfg = {"project": "g/s/r", "access_token": "tkn"}

    # Manager with a default override ref
    mgr = GitLabPromptManager(cfg, ref="manager-default")

    # Provide both git_ref kwarg and prompt_version, the latter should win
    msgs, params = mgr.pre_call_hook(
        user_id="u",
        messages=[],
        litellm_params={},
        prompt_id="promptA",
        prompt_variables={"q": "hello"},
        prompt_version="sha-111",  # highest precedence
        git_ref="feature/branch-xyz",  # should be ignored because prompt_version provided
    )

    mock_client.get_file_content.assert_any_call("promptA.prompt", ref="sha-111")
    # sanity — prompt parsed and params returned
    assert any(m["role"] == "user" for m in msgs)
    assert params.get("model") == "gpt-4"


@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_version_ref_kwarg_used_when_no_prompt_version(mock_client_class):
    """
    If prompt_version is omitted, git_ref kwarg should be used.
    """
    mock_client = MagicMock()
    mock_client.get_file_content.return_value = "User: {{q}}"
    mock_client_class.return_value = mock_client

    cfg = {"project": "g/s/r", "access_token": "tkn"}
    mgr = GitLabPromptManager(cfg, ref="fallback-manager-ref")

    _msgs, _params = mgr.pre_call_hook(
        user_id="u",
        messages=[],
        litellm_params={},
        prompt_id="promptB",
        prompt_variables={"q": "hi"},
        git_ref="hotfix/ref-2",  # used since prompt_version not provided
    )

    mock_client.get_file_content.assert_any_call("promptB.prompt", ref="hotfix/ref-2")


@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_version_manager_override_used_when_no_prompt_version_or_kwarg(
    mock_client_class,
):
    """
    If neither prompt_version nor git_ref is supplied, fall back to manager-level ref override.
    """
    mock_client = MagicMock()
    mock_client.get_file_content.return_value = "User: {{q}}"
    mock_client_class.return_value = mock_client

    cfg = {"project": "g/s/r", "access_token": "tkn"}
    mgr = GitLabPromptManager(cfg, ref="manager-override-ref")

    _msgs, _params = mgr.pre_call_hook(
        user_id="u",
        messages=[],
        litellm_params={},
        prompt_id="promptC",
        prompt_variables={"q": "hey"},
    )

    mock_client.get_file_content.assert_any_call(
        "promptC.prompt", ref="manager-override-ref"
    )


@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_get_prompt_template_explicit_ref_param(mock_client_class):
    """
    Directly calling get_prompt_template(ref=...) should pass that ref to GitLabClient.
    """
    mock_client = MagicMock()
    mock_client.get_file_content.return_value = """---
model: gpt-4o
---
User: {{x}}"""
    mock_client_class.return_value = mock_client

    cfg = {"project": "g/s/r", "access_token": "tkn"}
    mgr = GitLabPromptManager(cfg)

    rendered, metadata = mgr.get_prompt_template(
        prompt_id="promptD",
        prompt_variables={"x": "value"},
        ref="v1.2.3",  # explicit tag
    )
    mock_client.get_file_content.assert_any_call("promptD.prompt", ref="v1.2.3")
    assert "value" in rendered
    assert metadata.get("model") == "gpt-4o"


@patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient")
def test_gitlab_prompt_version_with_prompts_path(mock_client_class):
    """
    Ensure prompts_path + prompt_version work together (path resolution + ref).
    """
    mock_client = MagicMock()
    mock_client.get_file_content.return_value = "User: {{q}}"
    mock_client_class.return_value = mock_client

    cfg = {
        "project": "g/s/r",
        "access_token": "tkn",
        "prompts_path": "prompts/chat",
    }
    mgr = GitLabPromptManager(cfg)

    _msgs, _params = mgr.pre_call_hook(
        user_id="u",
        messages=[],
        litellm_params={},
        prompt_id="folder/sub/my_prompt",
        prompt_variables={"q": "ok"},
        prompt_version="commit-sha-999",
    )

    # Path should include prompts_path and end with .prompt
    mock_client.get_file_content.assert_any_call(
        "prompts/chat/folder/sub/my_prompt.prompt", ref="commit-sha-999"
    )
