"""Tests for ContextBuilder — system prompt and message assembly."""

from pathlib import Path

import pytest

from nanobot.agent.context import ContextBuilder, TranscriptInput
from nanobot.runtime_context import RuntimeContextBlock

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _builder(tmp_path: Path, **kw) -> ContextBuilder:
    return ContextBuilder(workspace=tmp_path, **kw)


# ---------------------------------------------------------------------------
# _merge_message_content (static)
# ---------------------------------------------------------------------------


class TestMergeMessageContent:
    def test_str_plus_str(self):
        result = ContextBuilder._merge_message_content("hello", "world")
        assert result == "hello\n\nworld"

    def test_empty_left_plus_str(self):
        result = ContextBuilder._merge_message_content("", "world")
        assert result == "world"

    def test_list_plus_list(self):
        left = [{"type": "text", "text": "a"}]
        right = [{"type": "text", "text": "b"}]
        result = ContextBuilder._merge_message_content(left, right)
        assert len(result) == 2
        assert result[0]["text"] == "a"
        assert result[1]["text"] == "b"

    def test_str_plus_list(self):
        right = [{"type": "text", "text": "b"}]
        result = ContextBuilder._merge_message_content("hello", right)
        assert len(result) == 2
        assert result[0]["text"] == "hello"
        assert result[1]["text"] == "b"

    def test_list_plus_str(self):
        left = [{"type": "text", "text": "a"}]
        result = ContextBuilder._merge_message_content(left, "world")
        assert len(result) == 2
        assert result[0]["text"] == "a"
        assert result[1]["text"] == "world"

    def test_none_plus_str(self):
        result = ContextBuilder._merge_message_content(None, "hello")
        assert result == [{"type": "text", "text": "hello"}]

    def test_str_plus_none(self):
        result = ContextBuilder._merge_message_content("hello", None)
        assert result == [{"type": "text", "text": "hello"}]

    def test_none_plus_none(self):
        result = ContextBuilder._merge_message_content(None, None)
        assert result == []

    def test_list_items_not_dicts_wrapped(self):
        result = ContextBuilder._merge_message_content(["raw_item"], None)
        assert result == [{"type": "text", "text": "raw_item"}]


# ---------------------------------------------------------------------------
# _load_bootstrap_files
# ---------------------------------------------------------------------------


class TestLoadBootstrapFiles:
    def test_no_bootstrap_files(self, tmp_path):
        builder = _builder(tmp_path)
        assert builder._load_bootstrap_files() == ""

    def test_empty_bootstrap_files(self, tmp_path):
        (tmp_path / "AGENTS.md").write_text("\n", encoding="utf-8")
        builder = _builder(tmp_path)
        assert builder._load_bootstrap_files() == ""

    def test_agents_md(self, tmp_path):
        (tmp_path / "AGENTS.md").write_text("Be helpful.", encoding="utf-8")
        builder = _builder(tmp_path)
        result = builder._load_bootstrap_files()
        assert "## AGENTS.md" in result
        assert "Be helpful." in result

    def test_multiple_bootstrap_files(self, tmp_path):
        (tmp_path / "AGENTS.md").write_text("Rules.", encoding="utf-8")
        (tmp_path / "SOUL.md").write_text("Soul.", encoding="utf-8")
        builder = _builder(tmp_path)
        result = builder._load_bootstrap_files()
        assert "## AGENTS.md" in result
        assert "## SOUL.md" in result
        assert "Rules." in result
        assert "Soul." in result

    def test_all_bootstrap_files(self, tmp_path):
        for name in ContextBuilder.BOOTSTRAP_FILES:
            (tmp_path / name).write_text(f"Content of {name}", encoding="utf-8")
        builder = _builder(tmp_path)
        result = builder._load_bootstrap_files()
        for name in ContextBuilder.BOOTSTRAP_FILES:
            assert f"## {name}" in result

    def test_legacy_tools_md_is_not_bootstrapped(self, tmp_path):
        (tmp_path / "TOOLS.md").write_text("workspace tool notes", encoding="utf-8")
        builder = _builder(tmp_path)
        result = builder._load_bootstrap_files()
        assert "TOOLS.md" not in result
        assert "workspace tool notes" not in result

    def test_utf8_content(self, tmp_path):
        (tmp_path / "AGENTS.md").write_text("用中文回复", encoding="utf-8")
        builder = _builder(tmp_path)
        result = builder._load_bootstrap_files()
        assert "用中文回复" in result

    def test_selected_project_supplies_only_agents_file(self, tmp_path):
        agent_home = tmp_path / "agent-home"
        project = tmp_path / "project"
        agent_home.mkdir()
        project.mkdir()
        (agent_home / "AGENTS.md").write_text("global project rules", encoding="utf-8")
        (agent_home / "SOUL.md").write_text("global soul", encoding="utf-8")
        (agent_home / "USER.md").write_text("global user", encoding="utf-8")
        (project / "AGENTS.md").write_text("selected project rules", encoding="utf-8")
        (project / "SOUL.md").write_text("project soul collision", encoding="utf-8")
        (project / "USER.md").write_text("project user collision", encoding="utf-8")

        result = ContextBuilder(agent_home).build_system_prompt(workspace=project)

        assert "selected project rules" in result
        assert "global project rules" not in result
        assert "global soul" in result
        assert "global user" in result
        assert "project soul collision" not in result
        assert "project user collision" not in result

    def test_selected_project_without_agents_does_not_fall_back(self, tmp_path):
        agent_home = tmp_path / "agent-home"
        project = tmp_path / "project"
        agent_home.mkdir()
        project.mkdir()
        (agent_home / "AGENTS.md").write_text("default workspace rules", encoding="utf-8")

        result = ContextBuilder(agent_home).build_system_prompt(workspace=project)

        assert "default workspace rules" not in result

    def test_unmodified_agents_and_user_templates_are_skipped(self, tmp_path):
        from nanobot.utils.helpers import sync_workspace_templates

        sync_workspace_templates(tmp_path, silent=True)

        result = ContextBuilder(tmp_path)._load_bootstrap_files()

        assert "## AGENTS.md" not in result
        assert "## USER.md" not in result
        assert "## SOUL.md" in result

    def test_customized_user_template_is_loaded(self, tmp_path):
        from nanobot.utils.helpers import sync_workspace_templates

        sync_workspace_templates(tmp_path, silent=True)
        (tmp_path / "USER.md").write_text("User prefers Chinese.", encoding="utf-8")

        result = ContextBuilder(tmp_path)._load_bootstrap_files()

        assert "## USER.md" in result
        assert "User prefers Chinese." in result


# ---------------------------------------------------------------------------
# _is_template_content (static)
# ---------------------------------------------------------------------------


class TestIsTemplateContent:
    def test_nonexistent_template_returns_false(self):
        assert ContextBuilder._is_template_content("anything", "nonexistent/path.md") is False

    def test_content_matching_template(self):
        from importlib.resources import files as pkg_files
        tpl = pkg_files("nanobot") / "templates" / "memory" / "MEMORY.md"
        if not tpl.is_file():
            pytest.skip("MEMORY.md template not bundled")
        original = tpl.read_text(encoding="utf-8")
        assert ContextBuilder._is_template_content(original, "memory/MEMORY.md") is True

    def test_modified_content_returns_false(self):
        from importlib.resources import files as pkg_files
        tpl = pkg_files("nanobot") / "templates" / "memory" / "MEMORY.md"
        if not tpl.is_file():
            pytest.skip("MEMORY.md template not bundled")
        assert ContextBuilder._is_template_content("totally different", "memory/MEMORY.md") is False


# ---------------------------------------------------------------------------
# Bundled bootstrap templates
# ---------------------------------------------------------------------------


class TestBundledToolContract:
    def test_tool_contract_balances_general_and_coding_workflows(self):
        from importlib.resources import files as pkg_files

        tpl = pkg_files("nanobot") / "templates" / "agent" / "tool_contract.md"
        content = tpl.read_text(encoding="utf-8")

        assert "## General Tool Contract" in content
        assert "Use the narrowest structured tool" in content
        assert "Do not use `exec` as a universal workaround" in content
        assert "## File and Coding Workflows" in content
        assert "`grep` returns matches with five context lines by default" in content
        assert 'defaults to `output_mode="files_with_matches"`' not in content
        assert "apply_patch" in content
        assert "acceptance criteria into concrete checks" in content
        assert "visual evidence reaches the model" in content
        assert "clear user request as authorization" in content
        assert "Never invent missing records or measurements" in content
        assert "scientific fitting" not in content
        assert "## Web and External Information" in content
        assert "## Messaging and Media" in content
        assert "## Scheduling and Background Work" in content
        assert "pure coding" not in content.lower()

    def test_tool_contract_is_injected_without_workspace_file(self, tmp_path):
        builder = _builder(tmp_path)
        prompt = builder.build_system_prompt()

        assert "# Tool Usage Notes" in prompt
        assert "## General Tool Contract" in prompt
        assert "Do not use `exec` as a universal workaround" in prompt


# ---------------------------------------------------------------------------
# build_user_content
# ---------------------------------------------------------------------------


class TestBuildUserContent:
    def test_no_media_returns_string(self, tmp_path):
        builder = _builder(tmp_path)
        result = builder.build_user_content("hello", None)
        assert result == "hello"

    def test_empty_media_returns_string(self, tmp_path):
        builder = _builder(tmp_path)
        result = builder.build_user_content("hello", [])
        assert result == "hello"

    def test_nonexistent_media_file_returns_string(self, tmp_path):
        builder = _builder(tmp_path)
        result = builder.build_user_content("hello", ["/nonexistent/image.png"])
        assert result == "hello"

    def test_non_image_file_returns_string(self, tmp_path):
        txt = tmp_path / "doc.txt"
        txt.write_text("not an image", encoding="utf-8")
        builder = _builder(tmp_path)
        result = builder.build_user_content("hello", [str(txt)])
        assert result == "hello"

    def test_valid_image_returns_list(self, tmp_path):
        png = tmp_path / "test.png"
        png.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 16)
        builder = _builder(tmp_path)
        result = builder.build_user_content("hello", [str(png)])
        assert isinstance(result, list)
        assert len(result) == 2
        assert result[0]["type"] == "image_url"
        assert result[0]["image_url"]["url"].startswith("data:image/png;base64,")
        assert result[1]["type"] == "text"
        assert result[1]["text"] == "hello"

    def test_image_meta_includes_path(self, tmp_path):
        png = tmp_path / "test.png"
        png.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 16)
        builder = _builder(tmp_path)
        result = builder.build_user_content("hello", [str(png)])
        assert "_meta" in result[0]
        assert "path" in result[0]["_meta"]


# ---------------------------------------------------------------------------
# build_system_prompt
# ---------------------------------------------------------------------------


class TestBuildSystemPrompt:
    def test_returns_nonempty_string(self, tmp_path):
        builder = _builder(tmp_path)
        result = builder.build_system_prompt()
        assert isinstance(result, str)
        assert len(result) > 0

    def test_includes_identity_section(self, tmp_path):
        builder = _builder(tmp_path)
        result = builder.build_system_prompt()
        assert "workspace" in result.lower() or "python" in result.lower()

    def test_default_identity_uses_relative_agent_paths(self, tmp_path):
        result = ContextBuilder(tmp_path)._get_identity()

        assert str(tmp_path.resolve()) not in result
        assert "Agent profile: SOUL.md and USER.md" in result
        assert "History log: memory/history.jsonl" in result
        assert "Custom skills: skills/{skill-name}/SKILL.md" in result

    def test_selected_project_identity_keeps_agent_data_in_agent_workspace(self, tmp_path):
        agent_home = tmp_path / "agent-home"
        project = tmp_path / "project"
        agent_home.mkdir()
        project.mkdir()

        result = ContextBuilder(agent_home)._get_identity(workspace=project)

        assert str(project.resolve()) not in result
        assert f"agent workspace is at: {agent_home.resolve()}" in result
        assert f"{agent_home.resolve()}/SOUL.md" in result
        assert f"{project.resolve()}/SOUL.md" not in result

    def test_includes_bootstrap_files(self, tmp_path):
        (tmp_path / "AGENTS.md").write_text("Be helpful and concise.", encoding="utf-8")
        builder = _builder(tmp_path)
        result = builder.build_system_prompt()
        assert "Be helpful and concise." in result

    def test_includes_session_summary(self, tmp_path):
        builder = _builder(tmp_path)
        summary = {
            "text": "Previous chat about Python.",
            "last_active": "2026-08-19T10:00:00",
        }
        result = builder.build_system_prompt(session_summary=summary)
        assert "Previous chat about Python." in result
        assert "[Archived Context Summary]" in result

    def test_sections_separated_by_separator(self, tmp_path):
        (tmp_path / "AGENTS.md").write_text("Rules.", encoding="utf-8")
        builder = _builder(tmp_path)
        summary = {"text": "Summary.", "last_active": "2026-08-19T10:00:00"}
        result = builder.build_system_prompt(session_summary=summary)
        assert "\n\n---\n\n" in result

    def test_no_bootstrap_no_summary(self, tmp_path):
        builder = _builder(tmp_path)
        result = builder.build_system_prompt()
        assert "## AGENTS.md" not in result
        assert "[Archived Context Summary]" not in result


# ---------------------------------------------------------------------------
# build_messages
# ---------------------------------------------------------------------------


class TestBuildMessages:
    def test_optional_arguments_are_keyword_only(self, tmp_path):
        builder = _builder(tmp_path)

        with pytest.raises(TypeError):
            builder.build_system_prompt(["legacy-skill"])
        with pytest.raises(TypeError):
            builder.build_messages([], "hello", ["legacy-skill"])

    def test_basic_empty_history(self, tmp_path):
        builder = _builder(tmp_path)
        messages = builder.build_messages([], "hello")
        assert len(messages) == 2
        assert messages[0]["role"] == "system"
        assert messages[1]["role"] == "user"
        assert "hello" in str(messages[1]["content"])

    def test_public_builder_preserves_assistant_role_compatibility(self, tmp_path):
        from nanobot.agent import ContextBuilder as PublicContextBuilder

        builder = PublicContextBuilder(tmp_path)
        messages = builder.build_messages(
            history=[{"role": "assistant", "content": "previous result"}],
            current_message="subagent result",
            current_role="assistant",
            runtime_context_blocks=[
                RuntimeContextBlock(source="test", content="user-only runtime context"),
            ],
        )

        assert len(messages) == 2
        assert messages[-1]["role"] == "assistant"
        assert messages[-1]["content"] == "previous result\n\nsubagent result"
        assert "user-only runtime context" not in messages[-1]["content"]
        assert "_meta" not in messages[-1]

    def test_compatibility_builder_merges_system_role_without_history(self, tmp_path):
        builder = _builder(tmp_path)

        messages = builder.build_messages([], "system event", current_role="system")

        assert len(messages) == 1
        assert messages[0]["role"] == "system"
        assert str(messages[0]["content"]).endswith("system event")

    def test_explicit_skill_reference_loads_full_instructions_for_this_turn(self, tmp_path):
        skill_dir = tmp_path / "skills" / "review"
        skill_dir.mkdir(parents=True)
        (skill_dir / "SKILL.md").write_text(
            "---\n"
            "name: review\n"
            "description: Review changes.\n"
            "---\n\n"
            "# Review workflow\n\nFollow the unique review checklist.",
            encoding="utf-8",
        )
        builder = _builder(tmp_path)

        messages = builder.build_messages([], "Please $review this patch and use $review carefully.")
        plain_messages = builder.build_messages([], "Please review this patch carefully.")

        system_prompt = messages[0]["content"]
        user_prompt = messages[-1]["content"]
        assert system_prompt == plain_messages[0]["content"]
        assert "Follow the unique review checklist." not in system_prompt
        assert "Please $review this patch" in user_prompt
        assert "[Active Skills — instructions for this user turn]" in user_prompt
        assert "### Skill: review" in user_prompt
        assert "Follow the unique review checklist." in user_prompt
        assert user_prompt.count("### Skill: review") == 1
        assert messages[-1]["_meta"]["runtime_context"]["sources"] == [
            "explicit_skills"
        ]

    def test_unknown_skill_reference_does_not_change_active_skills(self, tmp_path):
        messages = _builder(tmp_path).build_messages([], "Keep the shell literal $HOME.")

        assert "# Active Skills" not in messages[0]["content"]

    def test_runtime_context_is_not_injected_by_default(self, tmp_path):
        builder = _builder(tmp_path)
        messages = builder.build_messages([], "hello", channel="cli")
        user_msg = str(messages[-1]["content"])
        assert user_msg == "hello"

    def test_explicit_runtime_context_blocks_are_appended(self, tmp_path):
        builder = _builder(tmp_path)
        messages = builder.build_messages(
            [],
            "please use @zoom tonight",
            runtime_context_blocks=[
                RuntimeContextBlock(
                    source="cli_apps",
                    content="CLI App Attachment: @zoom (installed; tool=run_cli_app).",
                ),
            ],
        )
        user_msg = str(messages[-1]["content"])

        assert "CLI App Attachment: @zoom" in user_msg
        assert "tool=run_cli_app" in user_msg
        assert user_msg.index("please use @zoom tonight") < user_msg.index(
            "CLI App Attachment: @zoom"
        )
        assert messages[-1]["_meta"]["runtime_context"]["sources"] == ["cli_apps"]

    def test_consecutive_same_role_merged(self, tmp_path):
        builder = _builder(tmp_path)
        history = [{"role": "user", "content": "previous user message"}]
        messages = builder.build_messages(history, "new message")
        assert len(messages) == 2  # system + merged user
        assert "previous user message" in str(messages[1]["content"])
        assert "new message" in str(messages[1]["content"])

    def test_structured_transcript_preserves_fresh_turn_boundary(self, tmp_path):
        builder = _builder(tmp_path)
        transcript = TranscriptInput(
            history=[{"role": "user", "content": "previous user message"}],
            current_message="new message",
        )

        messages = builder.build_transcript(transcript)

        assert [message["role"] for message in messages] == ["system", "user", "user"]
        assert messages[-2]["content"] == "previous user message"
        assert messages[-1]["content"] == "new message"
        assert transcript.message_count == 3

    def test_current_message_can_be_built_without_history_merge(self, tmp_path):
        builder = _builder(tmp_path)
        current = builder.build_current_message(
            "new message",
            runtime_context_blocks=[
                RuntimeContextBlock(source="test", content="fresh context"),
            ],
        )

        assert current["role"] == "user"
        assert "new message" in current["content"]
        assert "fresh context" in current["content"]
        assert current["_meta"]["runtime_context"]["sources"] == ["test"]

    def test_different_role_appended(self, tmp_path):
        builder = _builder(tmp_path)
        history = [{"role": "assistant", "content": "previous response"}]
        messages = builder.build_messages(history, "new message")
        assert len(messages) == 3  # system + assistant + user

    def test_media_with_history(self, tmp_path):
        png = tmp_path / "img.png"
        png.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 16)
        builder = _builder(tmp_path)
        history = [{"role": "assistant", "content": "see this"}]
        messages = builder.build_messages(history, "check image", media=[str(png)])
        user_msg = messages[-1]["content"]
        assert isinstance(user_msg, list)
        assert any(b.get("type") == "image_url" for b in user_msg)
