from unittest.mock import patch

from django.contrib.auth import get_user_model
from django.test import TestCase, override_settings, tag

from api.agent.core.prompt_context import (
    _build_agent_capabilities_sections,
    _get_sandbox_prompt_summary,
    build_prompt_context,
)
from api.agent.tools.run_command import get_run_command_tool
from api.agent.tools.spawn_web_task import get_spawn_web_task_tool
from api.agent.tools.web_chat_sender import get_send_chat_tool
from api.models import BrowserUseAgent, CommsAllowlistEntry, PersistentAgent
from api.services.web_sessions import start_web_session
from billing.addons import AddonUplift


@tag("batch_promptree")
class AgentCapabilitiesPromptTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        User = get_user_model()
        cls.user = User.objects.create_user(
            username="cap-user",
            email="cap-user@example.com",
            password="pass1234",
        )
        browser_agent = BrowserUseAgent.objects.create(
            user=cls.user,
            name="Browser Agent",
        )
        cls.agent = PersistentAgent.objects.create(
            user=cls.user,
            name="Capability Agent",
            browser_use_agent=browser_agent,
        )

    @override_settings(PUBLIC_SITE_URL="https://app.test")
    @patch("api.agent.core.prompt_context.DedicatedProxyService.allocated_count", return_value=2)
    @patch("api.agent.core.prompt_context.AddonEntitlementService.get_uplift")
    @patch("api.agent.core.prompt_context.get_owner_plan")
    def test_capabilities_block_includes_plan_addons_and_links(
        self,
        plan_mock,
        uplift_mock,
        _dedicated_mock,
    ):
        plan_mock.return_value = {
            "id": "startup",
            "name": "Pro",
            "max_contacts_per_agent": 20,
        }
        uplift_mock.return_value = AddonUplift(
            task_credits=2000,
            contact_cap=10,
            browser_task_daily=5,
            advanced_captcha_resolution=1,
        )

        CommsAllowlistEntry.objects.create(
            agent=self.agent,
            channel="email",
            address="a@example.com",
            is_active=True,
            allow_inbound=True,
            allow_outbound=True,
        )

        sections = _build_agent_capabilities_sections(self.agent)
        capabilities_note = sections.get("agent_capabilities_note", "")
        plan_info = sections.get("plan_info", "")
        agent_addons = sections.get("agent_addons", "")
        agent_settings = sections.get("agent_settings", "")
        email_settings = sections.get("agent_email_settings", "")

        self.assertIn("plan/subscription info", capabilities_note)
        self.assertIn("Gobii account", capabilities_note)
        self.assertIn("agent settings available to the user", capabilities_note)
        self.assertIn("Plan: Pro", plan_info)
        self.assertIn("Available plans", plan_info)
        self.assertIn("Intelligence selection available", plan_info)
        self.assertIn(
            "Add-ons: +2000 credits; +10 contacts; +5 browser tasks/day; Advanced CAPTCHA resolution enabled.",
            plan_info,
        )
        self.assertIn("Per-agent contact cap: 30 (20 included in plan + add-ons", plan_info)
        self.assertIn("Contact usage: 1/30", plan_info)
        self.assertIn("Dedicated IPs purchased: 2", plan_info)
        self.assertIn("/console/billing/", plan_info)
        self.assertNotIn(f"/console/agents/{self.agent.id}/", plan_info)

        self.assertIn("Agent add-ons:", agent_addons)
        self.assertIn("Task pack: adds extra task credits", agent_addons)
        self.assertIn("Contact pack: increases the per-agent contact cap", agent_addons)
        self.assertIn("Browser task pack: increases the per-agent daily browser task limit", agent_addons)
        self.assertIn("Advanced CAPTCHA resolution: enables CapSolver-powered CAPTCHA solving", agent_addons)

        self.assertIn(f"/console/agents/{self.agent.id}/", agent_settings)
        self.assertIn("The agent settings UI is a single page.", agent_settings)
        self.assertIn("Do not invent subpage links", agent_settings)
        self.assertIn("Only use explicitly listed URLs", agent_settings)
        self.assertIn(f"/console/agents/{self.agent.id}/secrets/", agent_settings)
        self.assertIn(f"/console/agents/{self.agent.id}/email/", agent_settings)
        self.assertIn("Agent email settings", email_settings)
        self.assertIn("SMTP (outbound)", email_settings)
        self.assertIn("IMAP (inbound)", email_settings)
        self.assertIn(f"/console/agents/{self.agent.id}/email/", email_settings)

    @patch("api.agent.core.prompt_context.ensure_steps_compacted")
    @patch("api.agent.core.prompt_context.ensure_comms_compacted")
    def test_build_prompt_context_includes_secret_request_guidance(self, _mock_comms, _mock_steps):
        context, _, _ = build_prompt_context(self.agent)
        contents = "\n".join(message["content"] for message in context)

        self.assertIn("domain-scoped credentials for `http_request`", contents)
        self.assertIn("login credentials for `spawn_web_task`", contents)
        self.assertIn("`secret_type='env_var'`", contents)
        self.assertIn("`os.environ`", contents)
        self.assertIn("Avoid 2FA/MFA unless the user explicitly asks for it", contents)
        self.assertIn("those flows may hit system limitations", contents)

    @patch("api.agent.core.prompt_context.ensure_steps_compacted")
    @patch("api.agent.core.prompt_context.ensure_comms_compacted")
    def test_build_prompt_context_discourages_internal_progress_narration(self, _mock_comms, _mock_steps):
        context, _, _ = build_prompt_context(self.agent)
        contents = "\n".join(message["content"] for message in context)

        self.assertIn("never narrate internal reasoning", contents)
        self.assertIn("tool sequencing", contents)
        self.assertIn("User-facing question, blocker, config change, or finding", contents)
        self.assertNotIn("Progress update?", contents)

    @patch("api.agent.core.prompt_context.ensure_steps_compacted")
    @patch("api.agent.core.prompt_context.ensure_comms_compacted")
    def test_build_prompt_context_says_final_send_stops(self, _mock_comms, _mock_steps):
        context, _, _ = build_prompt_context(self.agent)
        contents = "\n".join(message["content"] for message in context)

        self.assertIn(
            "This tool sends the final answer/report and no work remains after it → will_continue_work=false",
            contents,
        )
        self.assertIn(
            "Send your final report to the user with will_continue_work=false on that final send tool",
            contents,
        )
        self.assertIn(
            "If you still need to mark the plan done after the report is already sent, call update_plan with will_continue_work=false",
            contents,
        )
        self.assertIn("Plain text is invisible and update_plan is not delivery", contents)
        self.assertIn("no extra turn, no announcement or confirmation message", contents)
        self.assertNotIn(
            "Need to send the user your answer, summary, or final report → will_continue_work=true",
            contents,
        )

    @patch("api.agent.core.prompt_context.ensure_steps_compacted")
    @patch("api.agent.core.prompt_context.ensure_comms_compacted")
    def test_implied_send_prompt_keeps_working_silent(self, _mock_comms, _mock_steps):
        start_web_session(self.agent, self.user)

        context, _, _ = build_prompt_context(self.agent)
        contents = "\n".join(message["content"] for message in context)

        self.assertIn("Your response text is a user message", contents)
        self.assertIn("monitoring targets/scope before setup", contents)
        self.assertIn("never search for it or refetch the same successful URL", contents)
        self.assertIn("update your ongoing charter/schedule", contents)
        self.assertIn("While working, respond with tool calls and no text", contents)
        self.assertIn("never status narration", contents)

    def test_web_chat_tool_description_discourages_internal_narration(self):
        tool = get_send_chat_tool()
        description = tool["function"]["description"]
        will_continue_description = tool["function"]["parameters"]["properties"]["will_continue_work"]["description"]

        self.assertIn("non-blocking context, config changes, or findings", description)
        self.assertIn("Do not use this for questions that block the task", description)
        self.assertIn("use request_human_input so the question is tracked", description)
        self.assertIn("Do not narrate what you will do next", description)
        self.assertIn("Never send a message solely to justify continuing work", will_continue_description)

    def test_run_command_tool_description_distinguishes_shell_paths(self):
        tool = get_run_command_tool()
        description = tool["function"]["description"]

        self.assertIn("Gobii filespace paths like /tools/foo.py", description)
        self.assertIn("are for Gobii tool arguments, not shell paths", description)
        self.assertIn("use relative paths from the workspace root such as tools/foo.py", description)
        self.assertIn("absolute shell paths like /workspace/tools/foo.py", description)
        self.assertIn("Do not run /tools/foo.py", description)

    def test_spawn_web_task_description_requires_browser_only_need(self):
        tool = get_spawn_web_task_tool(self.agent)
        description = tool["function"]["description"]

        self.assertIn("search/scrape/structured-data/API tools cannot answer", description)
        self.assertIn("rendered page state, JavaScript, login, or user interaction", description)

    @patch("api.agent.core.prompt_context.sandbox_compute_enabled_for_agent", return_value=True)
    def test_sandbox_summary_biases_toward_custom_tools_for_bulk_work(self, _mock_sandbox):
        summary = _get_sandbox_prompt_summary(self.agent)

        self.assertIn("Default mode for repetitive, paginated, or bulk work", summary)
        self.assertIn("Prefer a small custom tool for repetitive, paginated, or bulk work", summary)
        self.assertIn("bulk MCP/API fan-out", summary)
        self.assertIn("bulk SQLite writes", summary)
        self.assertIn("Those triggers are not exhaustive", summary)
        self.assertIn("err on the side of creating and using one", summary)
        self.assertIn("especially strong trigger", summary)
        self.assertIn("even if the user did not explicitly ask for a custom tool or mention SQLite", summary)
        self.assertIn("source_path='/tools/name.py'", summary)
        self.assertIn("source_code", summary)
        self.assertIn("Do not pass only source_path unless you already wrote that file", summary)
        self.assertIn("exact final line `if __name__ == '__main__': main(run)`", summary)
        self.assertIn("do not invoke custom_* with empty params", summary)
        self.assertIn("dedupe/format jobs expose input_table, output_table, and run_date", summary)
        self.assertIn("do not stop after seed/setup/preview", summary)
        self.assertIn("side_effects_completed", summary)
        self.assertIn("do_not_repeat_manually=true", summary)
        self.assertIn("source-code next_action text exactly", summary)
        self.assertIn("after the block exits the DB is closed", summary)
        self.assertIn("what changed or which outputs are ready", summary)
        self.assertIn("verification guidance", summary)
        self.assertIn("Do not repeat manually; verify read-only", summary)
        self.assertIn("direct_post_urls", summary)
        self.assertIn("scrape_ready_urls", summary)
        self.assertIn("accepted ready-to-use values", summary)
        self.assertIn("db.row_factory = sqlite3.Row", summary)
        self.assertIn("before any `db.execute(...).fetchall()`/SELECT", summary)
        self.assertIn("setting it after fetching does not convert existing tuple rows", summary)
        self.assertIn("not `row.get(...)`", summary)
        self.assertIn("datetime.now(timezone.utc)", summary)
        self.assertIn("not `datetime.timezone`", summary)

    @patch("api.agent.core.prompt_context.sandbox_compute_enabled_for_agent", return_value=True)
    def test_sandbox_summary_distinguishes_tool_paths_from_shell_paths(self, _mock_sandbox):
        summary = _get_sandbox_prompt_summary(self.agent)

        self.assertIn("Path rules: Gobii tool arguments", summary)
        self.assertIn("`read_file.path`, `create_custom_tool.source_path`, and message attachments", summary)
        self.assertIn("use filespace paths like `/tools/foo.py`", summary)
        self.assertIn("`run_command.command` is a shell command", summary)
        self.assertIn("relative paths from the workspace root such as `tools/foo.py`", summary)
        self.assertIn("absolute shell paths like `/workspace/tools/foo.py`", summary)
        self.assertIn("do not run `/tools/foo.py` directly", summary)
        self.assertIn("use `.` or omit it for the root", summary)
        self.assertIn("Prefer `ALL_PROXY` as the canonical proxy path", summary)
        self.assertIn("direct HTTPS tunneling", summary)
        self.assertIn("ctx.requests_proxies()", summary)
        self.assertIn("ctx.proxy_url()", summary)
        self.assertIn("not bare `requests`/`httpx`", summary)
        self.assertIn("`secure_credentials_request` using `secret_type='env_var'`", summary)
