"""Tests for the MetaScrape Python client."""

import json
import unittest
from unittest.mock import patch, MagicMock
from http.client import HTTPResponse
from io import BytesIO

from metascrape import MetaScrape, MetaScrapeError


def _mock_response(status, body):
    """Create a mock urllib response."""
    resp = MagicMock()
    resp.status = status
    resp.read.return_value = json.dumps(body).encode("utf-8")
    resp.__enter__ = MagicMock(return_value=resp)
    resp.__exit__ = MagicMock(return_value=False)
    return resp


def _mock_http_error(status, body, reason="Error"):
    """Create a mock urllib.error.HTTPError."""
    import urllib.error
    err = urllib.error.HTTPError(
        url="https://api.shanecode.org/v1/extract",
        code=status,
        msg=reason,
        hdrs=None,
        fp=BytesIO(json.dumps(body).encode("utf-8")),
    )
    return err


class TestMetaScrapeInit(unittest.TestCase):
    def test_requires_api_key(self):
        with self.assertRaises(ValueError):
            MetaScrape("")

    def test_accepts_api_key(self):
        ms = MetaScrape("msk_test123")
        self.assertIsNotNone(ms)

    def test_strips_trailing_slash_from_base_url(self):
        ms = MetaScrape("msk_test123", base_url="https://api.example.com/")
        self.assertEqual(ms._base_url, "https://api.example.com")


class TestExtract(unittest.TestCase):
    @patch("urllib.request.urlopen")
    def test_extract_returns_data(self, mock_urlopen):
        mock_urlopen.return_value = _mock_response(200, {
            "data": {
                "title": "Example Domain",
                "description": "An example site",
                "favicon": "https://example.com/favicon.ico",
            }
        })

        ms = MetaScrape("msk_test123")
        result = ms.extract("https://example.com")

        self.assertEqual(result["title"], "Example Domain")
        self.assertEqual(result["description"], "An example site")

    def test_extract_requires_url(self):
        ms = MetaScrape("msk_test123")
        with self.assertRaises(ValueError):
            ms.extract("")

    @patch("urllib.request.urlopen")
    def test_extract_sends_api_key_header(self, mock_urlopen):
        mock_urlopen.return_value = _mock_response(200, {"data": {}})

        ms = MetaScrape("msk_test123")
        ms.extract("https://example.com")

        req = mock_urlopen.call_args[0][0]
        self.assertEqual(req.get_header("X-api-key"), "msk_test123")

    @patch("urllib.request.urlopen")
    def test_extract_url_encodes_target(self, mock_urlopen):
        mock_urlopen.return_value = _mock_response(200, {"data": {}})

        ms = MetaScrape("msk_test123")
        ms.extract("https://example.com/path?q=hello world")

        req = mock_urlopen.call_args[0][0]
        self.assertIn("hello%20world", req.full_url)

    @patch("urllib.request.urlopen")
    def test_extract_raises_on_error(self, mock_urlopen):
        mock_urlopen.side_effect = _mock_http_error(
            401, {"error": "invalid API key"}
        )

        ms = MetaScrape("msk_bad_key")
        with self.assertRaises(MetaScrapeError) as ctx:
            ms.extract("https://example.com")

        self.assertEqual(ctx.exception.status_code, 401)
        self.assertEqual(ctx.exception.message, "invalid API key")


class TestUsage(unittest.TestCase):
    @patch("urllib.request.urlopen")
    def test_usage_returns_data(self, mock_urlopen):
        mock_urlopen.return_value = _mock_response(200, {
            "data": {"plan": "hobby", "used": 42, "limit": 1000, "remaining": 958}
        })

        ms = MetaScrape("msk_test123")
        result = ms.usage()

        self.assertEqual(result["plan"], "hobby")
        self.assertEqual(result["used"], 42)
        self.assertEqual(result["remaining"], 958)

    @patch("urllib.request.urlopen")
    def test_usage_raises_on_rate_limit(self, mock_urlopen):
        mock_urlopen.side_effect = _mock_http_error(
            429, {"error": "rate limit exceeded"}
        )

        ms = MetaScrape("msk_test123")
        with self.assertRaises(MetaScrapeError) as ctx:
            ms.usage()

        self.assertEqual(ctx.exception.status_code, 429)


class TestSignup(unittest.TestCase):
    @patch("urllib.request.urlopen")
    def test_signup_posts_credentials(self, mock_urlopen):
        mock_urlopen.return_value = _mock_response(200, {
            "message": "Account created"
        })

        result = MetaScrape.signup("test@example.com", "password123")

        req = mock_urlopen.call_args[0][0]
        self.assertEqual(req.method, "POST")
        self.assertIn("/v1/signup", req.full_url)
        body = json.loads(req.data.decode("utf-8"))
        self.assertEqual(body["email"], "test@example.com")
        self.assertEqual(body["password"], "password123")


class TestLogin(unittest.TestCase):
    @patch("urllib.request.urlopen")
    def test_login_returns_api_key(self, mock_urlopen):
        mock_urlopen.return_value = _mock_response(200, {
            "api_key": "msk_returned_key"
        })

        result = MetaScrape.login("test@example.com", "password123")

        self.assertEqual(result["api_key"], "msk_returned_key")

    @patch("urllib.request.urlopen")
    def test_login_posts_to_correct_endpoint(self, mock_urlopen):
        mock_urlopen.return_value = _mock_response(200, {"api_key": "msk_key"})

        MetaScrape.login("test@example.com", "password123")

        req = mock_urlopen.call_args[0][0]
        self.assertEqual(req.method, "POST")
        self.assertIn("/v1/login", req.full_url)


class TestMetaScrapeError(unittest.TestCase):
    def test_error_has_status_and_message(self):
        err = MetaScrapeError(429, "rate limit exceeded")
        self.assertEqual(err.status_code, 429)
        self.assertEqual(err.message, "rate limit exceeded")
        self.assertIn("429", str(err))
        self.assertIn("rate limit exceeded", str(err))


if __name__ == "__main__":
    unittest.main()
