"""Tests for the fetch_orchestration_templates tool contract."""

import json
from unittest.mock import patch, MagicMock

import pytest

from cloudsense_dx_mcp.tools.fetch_templates import (
    _clamp_workers,
    _escape_soql_string,
    _sanitize_filename,
    execute,
)


class TestFetchAllContract:
    """Verify fetch_all / template_names guardrails."""

    @pytest.mark.asyncio
    async def test_rejects_missing_both(self):
        result = json.loads(await execute({}))
        assert result["success"] is False
        assert "must provide" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_rejects_empty_template_names_without_fetch_all(self):
        result = json.loads(await execute({"template_names": []}))
        assert result["success"] is False

    @pytest.mark.asyncio
    async def test_rejects_both_template_names_and_fetch_all(self):
        result = json.loads(await execute({
            "template_names": ["Foo"],
            "fetch_all": True,
        }))
        assert result["success"] is False
        assert "cannot specify both" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_accepts_fetch_all_true(self):
        mock_org = {"org_alias": "test-org", "org_source": "session"}
        mock_creds = {
            "access_token": "tok", "instance_url": "https://x",
            "api_version": "60.0", "username": "u",
        }
        mock_records = {"records": [{"Name": "Template A"}]}
        single_result = {"name": "Template A", "success": True, "file": "/tmp/a.json"}

        with (
            patch("cloudsense_dx_mcp.tools.fetch_templates.resolve_org",
                  return_value=mock_org),
            patch("cloudsense_dx_mcp.tools.fetch_templates.get_org_credentials",
                  return_value=mock_creds),
            patch("cloudsense_dx_mcp.tools.fetch_templates.run_soql",
                  return_value=mock_records),
            patch("cloudsense_dx_mcp.tools.fetch_templates._fetch_single_template",
                  return_value=single_result),
            patch("pathlib.Path.mkdir"),
        ):
            result = json.loads(await execute({"fetch_all": True}))
            assert result["success"] is True
            assert "elapsed_seconds" in result

    @pytest.mark.asyncio
    async def test_fetch_all_skips_validation(self):
        mock_org = {"org_alias": "test-org", "org_source": "session"}
        mock_creds = {
            "access_token": "tok", "instance_url": "https://x",
            "api_version": "60.0", "username": "u",
        }
        mock_records = {"records": [{"Name": "A"}]}
        single_result = {"name": "A", "success": True, "file": "/tmp/a.json"}

        with (
            patch("cloudsense_dx_mcp.tools.fetch_templates.resolve_org",
                  return_value=mock_org),
            patch("cloudsense_dx_mcp.tools.fetch_templates.get_org_credentials",
                  return_value=mock_creds),
            patch("cloudsense_dx_mcp.tools.fetch_templates.run_soql",
                  return_value=mock_records),
            patch("cloudsense_dx_mcp.tools.fetch_templates._validate_template_names") as mock_validate,
            patch("cloudsense_dx_mcp.tools.fetch_templates._fetch_single_template",
                  return_value=single_result),
            patch("pathlib.Path.mkdir"),
        ):
            await execute({"fetch_all": True})
            mock_validate.assert_not_called()

    @pytest.mark.asyncio
    async def test_explicit_names_does_validate(self):
        mock_org = {"org_alias": "test-org", "org_source": "user"}
        mock_creds = {
            "access_token": "tok", "instance_url": "https://x",
            "api_version": "60.0", "username": "u",
        }
        mock_validate = {"found": ["Foo"], "not_found": [], "available": []}
        single_result = {"name": "Foo", "success": True, "file": "/tmp/foo.json"}

        with (
            patch("cloudsense_dx_mcp.tools.fetch_templates.resolve_org",
                  return_value=mock_org),
            patch("cloudsense_dx_mcp.tools.fetch_templates.get_org_credentials",
                  return_value=mock_creds),
            patch("cloudsense_dx_mcp.tools.fetch_templates._validate_template_names",
                  return_value=mock_validate) as mock_val,
            patch("cloudsense_dx_mcp.tools.fetch_templates._fetch_single_template",
                  return_value=single_result),
            patch("pathlib.Path.mkdir"),
        ):
            result = json.loads(await execute({"template_names": ["Foo"]}))
            assert result["success"] is True
            mock_val.assert_called_once()


class TestClampWorkers:
    def test_default_on_none(self):
        assert _clamp_workers(None) == 8

    def test_default_on_string(self):
        assert _clamp_workers("abc") == 8

    def test_minimum_is_one(self):
        assert _clamp_workers(0) == 1
        assert _clamp_workers(-5) == 1

    def test_clamps_to_upper_bound(self):
        assert _clamp_workers(999) <= 8

    def test_normal_value(self):
        assert _clamp_workers(3) == 3


class TestSanitizeFilename:
    def test_replaces_spaces(self):
        assert _sanitize_filename("Hello World") == "Hello_World"

    def test_replaces_special_chars(self):
        assert _sanitize_filename('a<b>c:d"e') == "a_b_c_d_e"

    def test_strips_leading_dots(self):
        assert _sanitize_filename("...test") == "test"

    def test_handles_empty_after_strip(self):
        assert _sanitize_filename("___") == ""


class TestEscapeSoqlString:
    def test_escapes_single_quote(self):
        assert _escape_soql_string("it's") == "it\\'s"

    def test_escapes_backslash(self):
        assert _escape_soql_string("a\\b") == "a\\\\b"

    def test_escapes_both(self):
        assert _escape_soql_string("it\\'s") == "it\\\\\\'s"

    def test_no_change_for_safe_string(self):
        assert _escape_soql_string("Hello World") == "Hello World"
