"""Tests for the Tokengrip client."""

import json
from unittest.mock import MagicMock, patch

import pytest

from tokengrip import Tokengrip


class TestInit:
    def test_requires_api_key(self):
        with pytest.raises(ValueError, match="api_key is required"):
            Tokengrip(api_key="")

    def test_default_config(self):
        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        assert tg._base_url == "https://tokengrip.com"
        assert tg._batch_size == 10
        tg.shutdown()

    def test_custom_config(self):
        tg = Tokengrip(
            api_key="tq_test_123",
            base_url="https://example.com/",
            project_id="proj",
            agent_name="bot",
            debug=True,
            batch_size=5,
            flush_interval_ms=0,
        )
        assert tg._base_url == "https://example.com"
        assert tg._project_id == "proj"
        assert tg._agent_name == "bot"
        assert tg._debug is True
        assert tg._batch_size == 5
        tg.shutdown()


class TestTrack:
    def test_buffers_record(self):
        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        tg.track(provider="openai", model="gpt-4.1", inputTokens=100, outputTokens=50)
        assert len(tg._buffer) == 1
        assert tg._buffer[0]["totalTokens"] == 150
        tg.shutdown()

    def test_accepts_dict(self):
        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        tg.track({"provider": "openai", "model": "gpt-4.1", "inputTokens": 10, "outputTokens": 5})
        assert len(tg._buffer) == 1
        assert tg._buffer[0]["provider"] == "openai"
        tg.shutdown()

    def test_applies_defaults(self):
        tg = Tokengrip(
            api_key="tq_test_123",
            project_id="proj",
            agent_name="bot",
            flush_interval_ms=0,
        )
        tg.track(provider="openai", model="gpt-4.1", inputTokens=10, outputTokens=5)
        assert tg._buffer[0]["projectId"] == "proj"
        assert tg._buffer[0]["agentName"] == "bot"
        tg.shutdown()

    def test_payload_override_beats_default(self):
        tg = Tokengrip(
            api_key="tq_test_123",
            project_id="default-proj",
            flush_interval_ms=0,
        )
        tg.track(
            provider="openai",
            model="gpt-4.1",
            inputTokens=10,
            outputTokens=5,
            projectId="override-proj",
        )
        assert tg._buffer[0]["projectId"] == "override-proj"
        tg.shutdown()

    def test_strips_falsy_optional_fields(self):
        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        tg.track(
            provider="openai",
            model="gpt-4.1",
            inputTokens=10,
            outputTokens=5,
            taskType="",
        )
        assert "taskType" not in tg._buffer[0]
        tg.shutdown()

    def test_dropped_after_shutdown(self):
        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        tg.shutdown()
        tg.track(provider="openai", model="gpt-4.1", inputTokens=10, outputTokens=5)
        assert len(tg._buffer) == 0

    def test_never_raises(self):
        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        # Passing a non-dict should not raise
        tg.track(None)  # type: ignore[arg-type]
        tg.shutdown()


def _mock_urlopen():
    mock_resp = MagicMock()
    mock_resp.status = 201
    mock_resp.__enter__ = MagicMock(return_value=mock_resp)
    mock_resp.__exit__ = MagicMock(return_value=False)
    return mock_resp


class TestFlush:
    @patch("tokengrip.client.urllib.request.urlopen")
    def test_sends_batch_as_array(self, mock_urlopen):
        mock_urlopen.return_value = _mock_urlopen()

        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        tg.track(provider="openai", model="gpt-4.1", inputTokens=100, outputTokens=50)
        tg.flush()

        mock_urlopen.assert_called_once()
        req = mock_urlopen.call_args[0][0]

        # Body must be a raw JSON array, not wrapped in { records: [...] }
        body = json.loads(req.data)
        assert isinstance(body, list)
        assert len(body) == 1
        assert body[0]["provider"] == "openai"
        assert body[0]["model"] == "gpt-4.1"
        assert body[0]["inputTokens"] == 100
        assert body[0]["outputTokens"] == 50
        assert body[0]["totalTokens"] == 150
        tg.shutdown()

    @patch("tokengrip.client.urllib.request.urlopen")
    def test_sends_correct_headers(self, mock_urlopen):
        mock_urlopen.return_value = _mock_urlopen()

        tg = Tokengrip(api_key="tq_test_key", flush_interval_ms=0)
        tg.track(provider="openai", model="gpt-4.1", inputTokens=10, outputTokens=5)
        tg.flush()

        req = mock_urlopen.call_args[0][0]
        assert req.get_header("Content-type") == "application/json"
        assert req.get_header("Authorization") == "Bearer tq_test_key"
        assert "tokengrip-python" in req.get_header("User-agent")
        tg.shutdown()

    @patch("tokengrip.client.urllib.request.urlopen")
    def test_sends_to_correct_url(self, mock_urlopen):
        mock_urlopen.return_value = _mock_urlopen()

        tg = Tokengrip(
            api_key="tq_test_123",
            base_url="https://example.com",
            flush_interval_ms=0,
        )
        tg.track(provider="openai", model="gpt-4.1", inputTokens=10, outputTokens=5)
        tg.flush()

        req = mock_urlopen.call_args[0][0]
        assert req.full_url == "https://example.com/api/v1/track"
        tg.shutdown()

    @patch("tokengrip.client.urllib.request.urlopen")
    def test_auto_flush_on_batch_size(self, mock_urlopen):
        mock_urlopen.return_value = _mock_urlopen()

        tg = Tokengrip(api_key="tq_test_123", batch_size=2, flush_interval_ms=0)
        tg.track(provider="openai", model="gpt-4.1", inputTokens=10, outputTokens=5)
        assert mock_urlopen.call_count == 0
        tg.track(provider="openai", model="gpt-4.1", inputTokens=10, outputTokens=5)
        assert mock_urlopen.call_count == 1
        tg.shutdown()

    @patch("tokengrip.client.urllib.request.urlopen")
    def test_batch_multiple_records(self, mock_urlopen):
        mock_urlopen.return_value = _mock_urlopen()

        tg = Tokengrip(api_key="tq_test_123", batch_size=3, flush_interval_ms=0)
        for i in range(3):
            tg.track(provider="openai", model=f"model-{i}", inputTokens=10, outputTokens=5)

        body = json.loads(mock_urlopen.call_args[0][0].data)
        assert len(body) == 3
        assert body[0]["model"] == "model-0"
        assert body[2]["model"] == "model-2"
        tg.shutdown()

    def test_empty_flush_is_noop(self):
        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        tg.flush()  # Should not raise
        tg.shutdown()

    @patch("tokengrip.client.urllib.request.urlopen")
    def test_flush_clears_buffer(self, mock_urlopen):
        mock_urlopen.return_value = _mock_urlopen()

        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        tg.track(provider="openai", model="gpt-4.1", inputTokens=10, outputTokens=5)
        tg.flush()
        assert len(tg._buffer) == 0
        tg.shutdown()

    @patch("tokengrip.client.urllib.request.urlopen")
    def test_flush_never_raises_on_http_error(self, mock_urlopen):
        import urllib.error

        mock_urlopen.side_effect = urllib.error.HTTPError(
            url="", code=500, msg="err", hdrs=None, fp=None  # type: ignore[arg-type]
        )

        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        tg.track(provider="openai", model="gpt-4.1", inputTokens=10, outputTokens=5)
        tg.flush()  # Should not raise
        tg.shutdown()


class TestShutdown:
    @patch("tokengrip.client.urllib.request.urlopen")
    def test_flushes_on_shutdown(self, mock_urlopen):
        mock_urlopen.return_value = _mock_urlopen()

        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        tg.track(provider="openai", model="gpt-4.1", inputTokens=10, outputTokens=5)
        tg.shutdown()
        assert mock_urlopen.call_count == 1
        assert len(tg._buffer) == 0

    def test_double_shutdown_is_safe(self):
        tg = Tokengrip(api_key="tq_test_123", flush_interval_ms=0)
        tg.shutdown()
        tg.shutdown()  # Should not raise
