"""
Unit tests for syndicate modules.

Tests chain_writer, session_summary, and game_scheduler.
Skipped when the agentic module is not available.
"""

import json
import os
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import datetime, timezone, timedelta
from dataclasses import asdict

pytest.importorskip("agentic", reason="agentic module not available in this environment")


# ============================================================================
# chain_writer — ERC-8021 Builder Code
# ============================================================================


class TestERC8021:
    def test_build_suffix_default(self):
        from agentic.syndicate.chain_writer import build_erc8021_suffix
        suffix = build_erc8021_suffix("luckyst")
        assert len(suffix) == 24
        # First byte = length of "luckyst" = 7
        assert suffix[0] == 7
        # Next 7 bytes = "luckyst"
        assert suffix[1:8] == b"luckyst"
        # Byte 8 = schema (0x00)
        assert suffix[8] == 0

    def test_build_suffix_custom_entity(self):
        from agentic.syndicate.chain_writer import build_erc8021_suffix
        suffix = build_erc8021_suffix("bot1")
        assert suffix[0] == 4
        assert suffix[1:5] == b"bot1"
        assert suffix[5] == 0  # schema byte

    def test_luckyst_suffix_constant(self):
        from agentic.syndicate.chain_writer import LUCKYST_BUILDER_SUFFIX
        assert len(LUCKYST_BUILDER_SUFFIX) == 24


# ============================================================================
# chain_writer — RPC helpers
# ============================================================================


class TestChainWriterRPC:
    @pytest.mark.asyncio
    async def test_rpc_call(self):
        from agentic.syndicate.chain_writer import _rpc_call
        with patch("agentic.syndicate.chain_writer.httpx.AsyncClient") as mock_client:
            mock_resp = MagicMock()
            mock_resp.json.return_value = {"jsonrpc": "2.0", "result": "0x1", "id": 1}
            mock_client.return_value.__aenter__ = AsyncMock(return_value=MagicMock(
                post=AsyncMock(return_value=mock_resp)
            ))
            mock_client.return_value.__aexit__ = AsyncMock()

            result = await _rpc_call("eth_blockNumber", [])
            assert result["result"] == "0x1"

    @pytest.mark.asyncio
    async def test_send_raw_tx_error(self):
        from agentic.syndicate.chain_writer import _send_raw_tx
        with patch("agentic.syndicate.chain_writer._rpc_call", new_callable=AsyncMock) as mock_rpc:
            mock_rpc.return_value = {"error": {"message": "insufficient funds"}}
            with pytest.raises(RuntimeError, match="TX failed"):
                await _send_raw_tx("0xdeadbeef")


# ============================================================================
# chain_writer — write_report_to_chain
# ============================================================================


class TestWriteReport:
    @pytest.mark.asyncio
    async def test_write_report_success(self):
        from agentic.syndicate.chain_writer import write_report_to_chain

        with patch("agentic.syndicate.chain_writer._get_nonce", new_callable=AsyncMock, return_value=5), \
             patch("agentic.syndicate.chain_writer._get_gas_price", new_callable=AsyncMock, return_value=100_000_000_000), \
             patch("agentic.syndicate.chain_writer._send_raw_tx", new_callable=AsyncMock, return_value="0xtxhash"), \
             patch("agentic.syndicate.chain_writer._get_tx_receipt", new_callable=AsyncMock, return_value={
                 "status": "0x1",
                 "blockNumber": "0xa",
                 "gasUsed": "0x5208",
             }), \
             patch("agentic.syndicate.chain_writer.Account.sign_transaction") as mock_sign:
            mock_sign.return_value = MagicMock(raw_transaction=b"\x00" * 32)

            result = await write_report_to_chain(
                encrypted_report=b"test-report-data",
                private_key="0x" + "ab" * 32,
                from_address="0x" + "00" * 20,
            )
            assert result.success is True
            assert result.tx_hash == "0xtxhash"
            assert result.block_number == 10

    @pytest.mark.asyncio
    async def test_write_report_reverted(self):
        from agentic.syndicate.chain_writer import write_report_to_chain

        with patch("agentic.syndicate.chain_writer._get_nonce", new_callable=AsyncMock, return_value=0), \
             patch("agentic.syndicate.chain_writer._get_gas_price", new_callable=AsyncMock, return_value=100), \
             patch("agentic.syndicate.chain_writer._send_raw_tx", new_callable=AsyncMock, return_value="0xfail"), \
             patch("agentic.syndicate.chain_writer._get_tx_receipt", new_callable=AsyncMock, return_value={
                 "status": "0x0",
                 "blockNumber": "0x1",
                 "gasUsed": "0x1",
             }), \
             patch("agentic.syndicate.chain_writer.Account.sign_transaction") as mock_sign:
            mock_sign.return_value = MagicMock(raw_transaction=b"\x00" * 32)

            result = await write_report_to_chain(b"data", "0x" + "ab" * 32, "0x" + "00" * 20)
            assert result.success is False
            assert result.error == "Transaction reverted"

    @pytest.mark.asyncio
    async def test_write_report_exception(self):
        from agentic.syndicate.chain_writer import write_report_to_chain

        with patch("agentic.syndicate.chain_writer._get_nonce", new_callable=AsyncMock, side_effect=Exception("network error")):
            result = await write_report_to_chain(b"data", "0x" + "ab" * 32, "0x" + "00" * 20)
            assert result.success is False
            assert "network error" in result.error


# ============================================================================
# chain_writer — fetch_tx_calldata
# ============================================================================


class TestFetchCalldata:
    @pytest.mark.asyncio
    async def test_fetch_strips_suffix(self):
        from agentic.syndicate.chain_writer import fetch_tx_calldata
        report_data = b"encrypted-report-bytes-here"
        suffix = b"\x00" * 24  # 24-byte suffix
        full_calldata = "0x" + (report_data + suffix).hex()

        with patch("agentic.syndicate.chain_writer._rpc_call", new_callable=AsyncMock) as mock_rpc:
            mock_rpc.return_value = {"result": {"input": full_calldata}}
            result = await fetch_tx_calldata("0xabc")
            assert result == report_data

    @pytest.mark.asyncio
    async def test_fetch_no_tx(self):
        from agentic.syndicate.chain_writer import fetch_tx_calldata
        with patch("agentic.syndicate.chain_writer._rpc_call", new_callable=AsyncMock) as mock_rpc:
            mock_rpc.return_value = {"result": None}
            result = await fetch_tx_calldata("0xnonexistent")
            assert result is None


# ============================================================================
# session_summary
# ============================================================================


class TestSessionSummary:
    def test_session_summary_to_json(self):
        from agentic.syndicate.session_summary import SessionSummary
        ss = SessionSummary(
            instance_id=1,
            script_type="auto1",
            markets=["KXBTC-25MAR05-YES"],
            start_time="2025-03-05T10:00:00",
            end_time="2025-03-05T11:00:00",
            duration_seconds=3600,
            cycles_completed=120,
        )
        j = json.loads(ss.to_json())
        assert j["instance_id"] == 1
        assert j["duration_seconds"] == 3600

    def test_session_summary_to_text(self):
        from agentic.syndicate.session_summary import SessionSummary, MarketResult
        ss = SessionSummary(
            instance_id=1,
            script_type="auto2",
            markets=["M1", "M2"],
            duration_seconds=1800,
            cycles_completed=60,
            market_results=[
                MarketResult(ticker="M1", side="no", position=50, total_traded=2000, avg_cost=40.0, fills_count=10),
                MarketResult(ticker="M2", side="no", position=50, total_traded=2500, avg_cost=50.0, fills_count=15),
            ],
            estimated_pnl_cents=500,
            narrative="Great session.",
        )
        text = ss.to_text()
        assert "Instance: #1" in text
        assert "30m" in text
        assert "M1" in text
        assert "500c" in text

    def test_template_narrative(self):
        from agentic.syndicate.session_summary import _template_narrative
        result = _template_narrative(["KXBTC"], 1200, 40)
        assert "20 minutes" in result
        assert "40 full cycles" in result
        assert "Syndicate eternal" in result

    @pytest.mark.asyncio
    async def test_generate_ai_narrative_no_key(self):
        from agentic.syndicate.session_summary import generate_ai_narrative
        old_key = os.environ.pop("ANTHROPIC_API_KEY", None)
        try:
            result = await generate_ai_narrative({}, ["M1"], 600, 20)
            assert "Syndicate eternal" in result
        finally:
            if old_key:
                os.environ["ANTHROPIC_API_KEY"] = old_key

    @pytest.mark.asyncio
    async def test_build_session_summary(self):
        from agentic.syndicate.session_summary import build_session_summary

        mock_api = AsyncMock()
        mock_api._request = AsyncMock(return_value={
            "market_positions": [{"ticker": "M1", "position": 10, "total_traded": 500}],
            "fills": [{"count": 5}, {"count": 3}],
        })

        start = (datetime.now(timezone.utc) - timedelta(minutes=30)).isoformat()
        summary = await build_session_summary(
            kalshi_api=mock_api,
            instance_id=42,
            markets=["M1"],
            script_type="auto1",
            start_time=start,
            cycles_completed=60,
        )
        assert summary.instance_id == 42
        assert len(summary.market_results) == 1
        assert summary.cycles_completed == 60

    @pytest.mark.asyncio
    async def test_fetch_session_data_handles_errors(self):
        from agentic.syndicate.session_summary import fetch_session_data
        mock_api = AsyncMock()
        mock_api._request = AsyncMock(side_effect=Exception("API down"))

        data = await fetch_session_data(mock_api, ["M1", "M2"])
        # Should not raise, returns empty data
        assert "M1" in data
        assert data["M1"]["positions"] == []


# ============================================================================
# game_scheduler
# ============================================================================


class TestGameScheduler:
    def test_game_entry_dataclass(self):
        from agentic.syndicate.game_scheduler import GameEntry
        g = GameEntry(markets=["M1", "M2"], start_time="2025-03-05T18:00:00Z")
        assert len(g.markets) == 2
        assert g.contract_size == 1

    def test_schedule_commitment_serialization(self):
        from agentic.syndicate.game_scheduler import ScheduleCommitment, GameEntry
        sc = ScheduleCommitment(
            schedule_id="abc123",
            games=[GameEntry(markets=["M1"], start_time="2025-03-05T18:00:00Z")],
            created_at="2025-03-05T17:00:00Z",
        )
        j = sc.to_json()
        parsed = ScheduleCommitment.from_json(j)
        assert parsed.schedule_id == "abc123"
        assert len(parsed.games) == 1
        assert parsed.games[0].markets == ["M1"]

    @pytest.mark.asyncio
    async def test_create_schedule_redis_only(self):
        from agentic.syndicate.game_scheduler import create_schedule, SCHEDULE_SET, SCHEDULE_META
        r = MagicMock()
        r.zadd = MagicMock()
        r.set = MagicMock()

        games = [
            {"markets": ["M1"], "start_time": "2025-03-05T18:30:00+00:00"},
            {"markets": ["M2", "M3"], "start_time": "2025-03-05T20:00:00+00:00"},
        ]
        commitment = await create_schedule(games, r)
        assert len(commitment.games) == 2
        assert commitment.base_tx is None  # no Base creds
        assert r.zadd.call_count == 2
        assert r.set.call_count == 3  # 2 games + 1 full commitment

    @pytest.mark.asyncio
    async def test_check_and_deploy(self):
        from agentic.syndicate.game_scheduler import check_and_deploy_scheduled_games, SCHEDULE_SET, SCHEDULE_META
        r = MagicMock()

        game_id = "sched1:0"
        meta = {
            "schedule_id": "sched1",
            "game_index": 0,
            "markets": ["M1"],
            "start_time": "2025-03-05T18:00:00+00:00",
            "stop_time": None,
            "contract_size": 1,
            "higher_first": False,
            "status": "pending",
            "base_tx": None,
        }
        r.zrangebyscore = MagicMock(return_value=[game_id])
        r.get = MagicMock(return_value=json.dumps(meta))
        r.set = MagicMock()
        r.sadd = MagicMock()
        r.zrem = MagicMock()
        r.zadd = MagicMock()

        deploy_fn = AsyncMock(return_value=99)

        deployed = await check_and_deploy_scheduled_games(r, deploy_fn)
        assert deployed == [game_id]
        deploy_fn.assert_called_once_with(markets=["M1"], contract_size=1, higher_first=False)

    @pytest.mark.asyncio
    async def test_check_and_stop(self):
        from agentic.syndicate.game_scheduler import check_and_stop_scheduled_games, SCHEDULE_META
        r = MagicMock()
        entry = json.dumps({"game_id": "s1:0", "instance_id": 42})
        r.zrangebyscore = MagicMock(return_value=[entry])
        r.get = MagicMock(return_value=json.dumps({"status": "running"}))
        r.set = MagicMock()
        r.srem = MagicMock()
        r.sadd = MagicMock()
        r.zrem = MagicMock()

        stop_fn = AsyncMock()
        await check_and_stop_scheduled_games(r, stop_fn)
        stop_fn.assert_called_once_with(42)

    def test_list_pending_games(self):
        from agentic.syndicate.game_scheduler import list_pending_games, SCHEDULE_SET, SCHEDULE_META
        r = MagicMock()
        r.zrange = MagicMock(return_value=[("s1:0", 1709660400.0)])
        r.get = MagicMock(return_value=json.dumps({"markets": ["M1"], "status": "pending"}))

        results = list_pending_games(r)
        assert len(results) == 1
        assert results[0]["markets"] == ["M1"]

    def test_list_active_games(self):
        from agentic.syndicate.game_scheduler import list_active_games
        r = MagicMock()
        r.smembers = MagicMock(return_value={"s1:0"})
        r.get = MagicMock(return_value=json.dumps({"markets": ["M2"], "status": "running"}))

        results = list_active_games(r)
        assert len(results) == 1
        assert results[0]["status"] == "running"
