"""Tests for game-plugin-oneshot.

Exercises every tool function, argument validation, JSON parsing branches,
error handling, state factory, and plugin structure.
"""

import json
from unittest.mock import MagicMock, patch

import pytest
from game_sdk.game.custom_types import Argument, Function, FunctionResultStatus
from game_sdk.game.agent import WorkerConfig


# ─── Fixtures ────────────────────────────────────────────────────────────────

@pytest.fixture
def mock_client():
    client = MagicMock()
    client.address = "0x1234567890abcdef1234567890abcdef12345678"
    client.chain_id = 8453
    return client


@pytest.fixture
def plugin(mock_client):
    with patch("game_plugin_oneshot.plugin.OneShotClient", return_value=mock_client):
        from game_plugin_oneshot import OneShotPlugin
        p = OneShotPlugin(private_key="0x" + "ab" * 32)
        p.client = mock_client
        return p


@pytest.fixture
def worker(plugin):
    return plugin.get_worker()


@pytest.fixture
def fns(worker):
    """Dict of fn_name -> Function (action_space is already a dict)."""
    return worker.action_space


# ─── Plugin structure ────────────────────────────────────────────────────────

class TestPluginStructure:
    def test_get_worker_returns_worker_config(self, worker):
        assert isinstance(worker, WorkerConfig)
        assert worker.id == "oneshot_worker"

    def test_worker_description_mentions_oneshot(self, worker):
        assert "OneShot" in worker.worker_description

    def test_worker_description_mentions_usdc(self, worker):
        assert "USDC" in worker.worker_description

    def test_worker_has_exactly_7_functions(self, fns):
        assert len(fns) == 7

    def test_all_values_are_Function_instances(self, fns):
        for fn in fns.values():
            assert isinstance(fn, Function)

    def test_function_names(self, fns):
        assert set(fns.keys()) == {
            "oneshot_email",
            "oneshot_sms",
            "oneshot_voice",
            "oneshot_research",
            "oneshot_commerce_search",
            "oneshot_commerce_buy",
            "oneshot_build",
        }

    def test_every_function_has_executable(self, fns):
        for fn in fns.values():
            assert callable(fn.executable)

    def test_every_function_has_description(self, fns):
        for fn in fns.values():
            assert len(fn.fn_description) > 20

    def test_every_function_has_args(self, fns):
        for fn in fns.values():
            assert len(fn.args) >= 1


# ─── Argument validation per tool ────────────────────────────────────────────

class TestArguments:
    def test_email_required_args(self, fns):
        args = {a.name: a for a in fns["oneshot_email"].args}
        assert args["to"].optional is False
        assert args["subject"].optional is False
        assert args["body"].optional is False
        assert args["from_domain"].optional is True

    def test_sms_required_args(self, fns):
        args = {a.name: a for a in fns["oneshot_sms"].args}
        assert args["to_number"].optional is False
        assert args["message"].optional is False

    def test_voice_required_and_optional(self, fns):
        args = {a.name: a for a in fns["oneshot_voice"].args}
        assert args["target_number"].optional is False
        assert args["objective"].optional is False
        assert args["caller_persona"].optional is True
        assert args["max_duration_minutes"].optional is True

    def test_research_args(self, fns):
        args = {a.name: a for a in fns["oneshot_research"].args}
        assert args["topic"].optional is False
        assert args["depth"].optional is True

    def test_commerce_search_args(self, fns):
        args = {a.name: a for a in fns["oneshot_commerce_search"].args}
        assert args["query"].optional is False
        assert args["limit"].optional is True

    def test_commerce_buy_args(self, fns):
        args = {a.name: a for a in fns["oneshot_commerce_buy"].args}
        assert args["product_url"].optional is False
        assert args["shipping_address"].optional is False
        assert args["quantity"].optional is True
        assert "JSON" in args["shipping_address"].description

    def test_build_args(self, fns):
        args = {a.name: a for a in fns["oneshot_build"].args}
        assert args["product"].optional is False
        assert args["type"].optional is True
        assert args["source_url"].optional is True
        assert "JSON" in args["product"].description

    def test_all_args_have_string_type(self, fns):
        """GAME Argument only supports string scalars."""
        for fn in fns.values():
            for arg in fn.args:
                assert arg.type == "string", f"{fn.fn_name}.{arg.name} type is {arg.type}"


# ─── Email tool ──────────────────────────────────────────────────────────────

class TestEmailTool:
    def test_success(self, fns, mock_client):
        mock_client.call_tool.return_value = {"request_id": "r1", "status": "processing"}
        status, msg, data = fns["oneshot_email"].executable(
            {"to": "a@b.com", "subject": "Hi", "body": "Hello"}, MagicMock()
        )
        assert status == FunctionResultStatus.DONE
        assert "a@b.com" in msg
        mock_client.call_tool.assert_called_once_with("/v1/tools/email/send", {
            "to": "a@b.com", "subject": "Hi", "body": "Hello",
        })

    def test_with_from_domain(self, fns, mock_client):
        mock_client.call_tool.return_value = {"request_id": "r2"}
        fns["oneshot_email"].executable(
            {"to": "a@b.com", "subject": "Hi", "body": "Hello", "from_domain": "acme.com"},
            MagicMock(),
        )
        call_payload = mock_client.call_tool.call_args[0][1]
        assert call_payload["from_domain"] == "acme.com"

    def test_without_from_domain(self, fns, mock_client):
        mock_client.call_tool.return_value = {"request_id": "r3"}
        fns["oneshot_email"].executable(
            {"to": "a@b.com", "subject": "Hi", "body": "Hello"}, MagicMock()
        )
        call_payload = mock_client.call_tool.call_args[0][1]
        assert "from_domain" not in call_payload

    def test_failure(self, fns, mock_client):
        mock_client.call_tool.side_effect = Exception("Payment failed")
        status, msg, data = fns["oneshot_email"].executable(
            {"to": "a@b.com", "subject": "Hi", "body": "Hello"}, MagicMock()
        )
        assert status == FunctionResultStatus.FAILED
        assert "Payment failed" in msg
        assert data == {}


# ─── SMS tool ────────────────────────────────────────────────────────────────

class TestSmsTool:
    def test_success(self, fns, mock_client):
        mock_client.call_tool.return_value = {"request_id": "s1"}
        status, msg, _ = fns["oneshot_sms"].executable(
            {"to_number": "+15551234567", "message": "test"}, MagicMock()
        )
        assert status == FunctionResultStatus.DONE
        assert "+15551234567" in msg

    def test_failure(self, fns, mock_client):
        mock_client.call_tool.side_effect = RuntimeError("Timeout")
        status, msg, _ = fns["oneshot_sms"].executable(
            {"to_number": "+15551234567", "message": "test"}, MagicMock()
        )
        assert status == FunctionResultStatus.FAILED
        assert "Timeout" in msg


# ─── Voice tool ──────────────────────────────────────────────────────────────

class TestVoiceTool:
    def test_success_basic(self, fns, mock_client):
        mock_client.call_tool.return_value = {"request_id": "v1", "transcript": "..."}
        status, msg, _ = fns["oneshot_voice"].executable(
            {"target_number": "+15559999999", "objective": "Schedule appointment"}, MagicMock()
        )
        assert status == FunctionResultStatus.DONE
        assert "+15559999999" in msg
        assert mock_client.call_tool.call_args[1]["timeout_sec"] == 300

    def test_with_optional_args(self, fns, mock_client):
        mock_client.call_tool.return_value = {"request_id": "v2"}
        fns["oneshot_voice"].executable(
            {
                "target_number": "+15559999999",
                "objective": "Follow up",
                "caller_persona": "Sales rep",
                "max_duration_minutes": "5",
            },
            MagicMock(),
        )
        payload = mock_client.call_tool.call_args[0][1]
        assert payload["caller_persona"] == "Sales rep"
        assert payload["max_duration_minutes"] == 5  # converted to int

    def test_failure(self, fns, mock_client):
        mock_client.call_tool.side_effect = Exception("No funds")
        status, msg, _ = fns["oneshot_voice"].executable(
            {"target_number": "+15559999999", "objective": "test"}, MagicMock()
        )
        assert status == FunctionResultStatus.FAILED


# ─── Research tool ───────────────────────────────────────────────────────────

class TestResearchTool:
    def test_success(self, fns, mock_client):
        mock_client.call_tool.return_value = {"report": "AI is growing..."}
        status, msg, data = fns["oneshot_research"].executable(
            {"topic": "AI trends 2026"}, MagicMock()
        )
        assert status == FunctionResultStatus.DONE
        assert "Research completed" in msg
        assert data["result"]["report"] == "AI is growing..."

    def test_with_depth(self, fns, mock_client):
        mock_client.call_tool.return_value = {}
        fns["oneshot_research"].executable(
            {"topic": "quantum computing", "depth": "quick"}, MagicMock()
        )
        payload = mock_client.call_tool.call_args[0][1]
        assert payload["depth"] == "quick"

    def test_without_depth(self, fns, mock_client):
        mock_client.call_tool.return_value = {}
        fns["oneshot_research"].executable({"topic": "test"}, MagicMock())
        payload = mock_client.call_tool.call_args[0][1]
        assert "depth" not in payload


# ─── Commerce Search tool (free) ─────────────────────────────────────────────

class TestCommerceSearchTool:
    def test_uses_free_get(self, fns, mock_client):
        mock_client.call_free_get.return_value = {"products": [{"name": "Widget"}]}
        status, msg, data = fns["oneshot_commerce_search"].executable(
            {"query": "headphones"}, MagicMock()
        )
        assert status == FunctionResultStatus.DONE
        mock_client.call_free_get.assert_called_once()
        mock_client.call_tool.assert_not_called()
        assert "headphones" in msg

    def test_with_limit(self, fns, mock_client):
        mock_client.call_free_get.return_value = {"products": []}
        fns["oneshot_commerce_search"].executable(
            {"query": "shoes", "limit": "5"}, MagicMock()
        )
        params = mock_client.call_free_get.call_args[0][1]
        assert params["limit"] == "5"

    def test_description_says_free(self, fns):
        assert "free" in fns["oneshot_commerce_search"].fn_description.lower()

    def test_failure(self, fns, mock_client):
        mock_client.call_free_get.side_effect = ConnectionError("offline")
        status, msg, _ = fns["oneshot_commerce_search"].executable(
            {"query": "test"}, MagicMock()
        )
        assert status == FunctionResultStatus.FAILED


# ─── Commerce Buy tool ───────────────────────────────────────────────────────

class TestCommerceBuyTool:
    VALID_ADDRESS = json.dumps({
        "first_name": "John",
        "last_name": "Doe",
        "street": "123 Main St",
        "city": "Austin",
        "state": "TX",
        "zip_code": "78701",
        "phone": "+15551234567",
    })

    def test_success(self, fns, mock_client):
        mock_client.call_tool.return_value = {"order_id": "ord_123"}
        status, msg, data = fns["oneshot_commerce_buy"].executable(
            {"product_url": "https://example.com/p", "shipping_address": self.VALID_ADDRESS},
            MagicMock(),
        )
        assert status == FunctionResultStatus.DONE
        assert "purchased" in msg.lower()

    def test_parses_json_address(self, fns, mock_client):
        mock_client.call_tool.return_value = {}
        fns["oneshot_commerce_buy"].executable(
            {"product_url": "https://example.com/p", "shipping_address": self.VALID_ADDRESS},
            MagicMock(),
        )
        payload = mock_client.call_tool.call_args[0][1]
        assert isinstance(payload["shipping_address"], dict)
        assert payload["shipping_address"]["city"] == "Austin"

    def test_with_quantity(self, fns, mock_client):
        mock_client.call_tool.return_value = {}
        fns["oneshot_commerce_buy"].executable(
            {"product_url": "https://example.com/p", "shipping_address": self.VALID_ADDRESS, "quantity": "3"},
            MagicMock(),
        )
        payload = mock_client.call_tool.call_args[0][1]
        assert payload["quantity"] == 3

    def test_uses_180s_timeout(self, fns, mock_client):
        mock_client.call_tool.return_value = {}
        fns["oneshot_commerce_buy"].executable(
            {"product_url": "https://example.com/p", "shipping_address": self.VALID_ADDRESS},
            MagicMock(),
        )
        assert mock_client.call_tool.call_args[1]["timeout_sec"] == 180

    def test_bad_json_address(self, fns, mock_client):
        status, msg, _ = fns["oneshot_commerce_buy"].executable(
            {"product_url": "https://example.com/p", "shipping_address": "not json"},
            MagicMock(),
        )
        assert status == FunctionResultStatus.FAILED
        assert "JSON" in msg

    def test_api_failure(self, fns, mock_client):
        mock_client.call_tool.side_effect = Exception("Card declined")
        status, msg, _ = fns["oneshot_commerce_buy"].executable(
            {"product_url": "https://example.com/p", "shipping_address": self.VALID_ADDRESS},
            MagicMock(),
        )
        assert status == FunctionResultStatus.FAILED
        assert "Card declined" in msg


# ─── Build tool ──────────────────────────────────────────────────────────────

class TestBuildTool:
    VALID_PRODUCT = json.dumps({"name": "Acme", "description": "A SaaS tool"})

    def test_success(self, fns, mock_client):
        mock_client.call_tool.return_value = {"url": "https://acme.vercel.app"}
        status, msg, data = fns["oneshot_build"].executable(
            {"product": self.VALID_PRODUCT}, MagicMock()
        )
        assert status == FunctionResultStatus.DONE
        assert "built" in msg.lower()

    def test_parses_json_product(self, fns, mock_client):
        mock_client.call_tool.return_value = {}
        fns["oneshot_build"].executable(
            {"product": self.VALID_PRODUCT}, MagicMock()
        )
        payload = mock_client.call_tool.call_args[0][1]
        assert isinstance(payload["product"], dict)
        assert payload["product"]["name"] == "Acme"

    def test_with_type_and_source(self, fns, mock_client):
        mock_client.call_tool.return_value = {}
        fns["oneshot_build"].executable(
            {"product": self.VALID_PRODUCT, "type": "portfolio", "source_url": "https://old.com"},
            MagicMock(),
        )
        payload = mock_client.call_tool.call_args[0][1]
        assert payload["type"] == "portfolio"
        assert payload["source_url"] == "https://old.com"

    def test_uses_600s_timeout(self, fns, mock_client):
        mock_client.call_tool.return_value = {}
        fns["oneshot_build"].executable(
            {"product": self.VALID_PRODUCT}, MagicMock()
        )
        assert mock_client.call_tool.call_args[1]["timeout_sec"] == 600

    def test_bad_json_product(self, fns, mock_client):
        status, msg, _ = fns["oneshot_build"].executable(
            {"product": "{broken"}, MagicMock()
        )
        assert status == FunctionResultStatus.FAILED
        assert "JSON" in msg

    def test_api_failure(self, fns, mock_client):
        mock_client.call_tool.side_effect = Exception("Build timeout")
        status, msg, _ = fns["oneshot_build"].executable(
            {"product": self.VALID_PRODUCT}, MagicMock()
        )
        assert status == FunctionResultStatus.FAILED
        assert "Build timeout" in msg


# ─── State factory ───────────────────────────────────────────────────────────

class TestStateFactory:
    def test_returns_wallet_address(self, worker, mock_client):
        state = worker.get_state_fn("", {})
        assert state["wallet_address"] == mock_client.address

    def test_returns_network_name(self, worker):
        state = worker.get_state_fn("", {})
        assert "Base Mainnet" == state["network"]

    def test_includes_cost_note(self, worker):
        state = worker.get_state_fn("", {})
        assert "USDC" in state["note"]
