"""Tests for lib/client.py — Hindsight REST API client."""

import json
import urllib.error
from io import BytesIO
from unittest.mock import MagicMock, patch

import pytest

from lib.client import HindsightClient, _validate_api_url


class TestValidateApiUrl:
    def test_valid_http(self):
        assert _validate_api_url("http://localhost:9077") == "http://localhost:9077"

    def test_valid_https(self):
        assert _validate_api_url("https://api.example.com/") == "https://api.example.com"

    def test_trailing_slash_stripped(self):
        assert _validate_api_url("http://host:8080/") == "http://host:8080"

    def test_invalid_scheme_raises(self):
        with pytest.raises(ValueError, match="http or https"):
            _validate_api_url("ftp://host")

    def test_no_hostname_raises(self):
        with pytest.raises(ValueError):
            _validate_api_url("http://")


class FakeResp:
    def __init__(self, data, status=200):
        self.status = status
        self._body = json.dumps(data).encode()

    def read(self):
        return self._body

    def __enter__(self):
        return self

    def __exit__(self, *_):
        pass


class TestHindsightClientInit:
    def test_rejects_non_http_url(self):
        with pytest.raises(ValueError):
            HindsightClient("ftp://bad")

    def test_stores_token(self):
        c = HindsightClient("http://localhost:9077", api_token="tok123")
        assert c.api_token == "tok123"

    def test_no_token(self):
        c = HindsightClient("http://localhost:9077")
        assert c.api_token is None


class TestHindsightClientRecall:
    def test_posts_to_correct_path(self):
        c = HindsightClient("http://localhost:9077")
        response_data = {"results": [{"text": "Paris", "type": "world"}]}
        with patch("urllib.request.urlopen", return_value=FakeResp(response_data)):
            resp = c.recall("my-bank", "capital of France")
        assert resp["results"][0]["text"] == "Paris"

    def test_bank_id_url_encoded(self):
        c = HindsightClient("http://localhost:9077")
        captured = {}

        def fake_open(req, timeout=None):
            captured["url"] = req.full_url
            return FakeResp({"results": []})

        with patch("urllib.request.urlopen", side_effect=fake_open):
            c.recall("bank with spaces", "query")

        assert "bank%20with%20spaces" in captured["url"]

    def test_includes_auth_header_when_token_set(self):
        c = HindsightClient("http://localhost:9077", api_token="mytoken")
        captured = {}

        def fake_open(req, timeout=None):
            captured["headers"] = dict(req.headers)
            return FakeResp({"results": []})

        with patch("urllib.request.urlopen", side_effect=fake_open):
            c.recall("bank", "query")

        assert "Authorization" in captured["headers"]
        assert "mytoken" in captured["headers"]["Authorization"]

    def test_no_auth_header_without_token(self):
        c = HindsightClient("http://localhost:9077")
        captured = {}

        def fake_open(req, timeout=None):
            captured["headers"] = dict(req.headers)
            return FakeResp({"results": []})

        with patch("urllib.request.urlopen", side_effect=fake_open):
            c.recall("bank", "query")

        assert "Authorization" not in captured["headers"]

    def test_http_error_raises_runtime_error(self):
        c = HindsightClient("http://localhost:9077")
        err = urllib.error.HTTPError(
            url="http://localhost:9077/v1/default/banks/b/memories/recall",
            code=500,
            msg="Internal Server Error",
            hdrs={},
            fp=BytesIO(b"server exploded"),
        )
        with patch("urllib.request.urlopen", side_effect=err):
            with pytest.raises(RuntimeError, match="HTTP 500"):
                c.recall("b", "query")

    def test_sends_budget_and_types(self):
        c = HindsightClient("http://localhost:9077")
        captured = {}

        def fake_open(req, timeout=None):
            captured["body"] = json.loads(req.data.decode())
            return FakeResp({"results": []})

        with patch("urllib.request.urlopen", side_effect=fake_open):
            c.recall("bank", "query", budget="high", types=["world", "experience"])

        assert captured["body"]["budget"] == "high"
        assert captured["body"]["types"] == ["world", "experience"]


class TestHindsightClientRetain:
    def test_posts_with_async_true(self):
        c = HindsightClient("http://localhost:9077")
        captured = {}

        def fake_open(req, timeout=None):
            captured["body"] = json.loads(req.data.decode())
            return FakeResp({"status": "accepted"})

        with patch("urllib.request.urlopen", side_effect=fake_open):
            c.retain("bank", "transcript content", document_id="doc-1", context="claude-code")

        assert captured["body"]["async"] is True
        assert captured["body"]["items"][0]["content"] == "transcript content"
        assert captured["body"]["items"][0]["context"] == "claude-code"

    def test_bank_id_encoded_in_retain_path(self):
        c = HindsightClient("http://localhost:9077")
        captured = {}

        def fake_open(req, timeout=None):
            captured["url"] = req.full_url
            return FakeResp({})

        with patch("urllib.request.urlopen", side_effect=fake_open):
            c.retain("my::bank", "content")

        assert "my%3A%3Abank" in captured["url"]


class TestHindsightClientHealthCheck:
    def test_returns_true_on_200(self):
        c = HindsightClient("http://localhost:9077")
        with patch("urllib.request.urlopen", return_value=FakeResp({}, status=200)):
            with patch("time.sleep"):  # don't actually sleep
                assert c.health_check() is True

    def test_returns_false_after_retries(self):
        c = HindsightClient("http://localhost:9077")
        with patch("urllib.request.urlopen", side_effect=OSError("refused")):
            with patch("time.sleep"):
                assert c.health_check() is False

    def test_retries_on_failure(self):
        c = HindsightClient("http://localhost:9077")
        call_count = 0

        def flaky(*_a, **_kw):
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                raise OSError("not yet")
            return FakeResp({}, status=200)

        with patch("urllib.request.urlopen", side_effect=flaky):
            with patch("time.sleep"):
                result = c.health_check()

        assert result is True
        assert call_count == 3


class TestHindsightClientSetBankMission:
    def test_patches_config_endpoint(self):
        c = HindsightClient("http://localhost:9077")
        captured = {}

        def fake_open(req, timeout=None):
            captured["url"] = req.full_url
            captured["method"] = req.method
            captured["body"] = json.loads(req.data.decode())
            return FakeResp({})

        with patch("urllib.request.urlopen", side_effect=fake_open):
            c.set_bank_mission("my-bank", "I am Claude", retain_mission="Extract facts")

        assert captured["method"] == "PATCH"
        assert "my-bank" in captured["url"]
        assert captured["body"]["updates"]["reflect_mission"] == "I am Claude"
        assert captured["body"]["updates"]["retain_mission"] == "Extract facts"
