"""Local HTTP fixtures for extract-website-data.py. No external API or credits."""
from copy import deepcopy
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import importlib.util
import json
from pathlib import Path
import threading
import sys
import unittest
from unittest.mock import patch
from urllib.error import URLError

sys.dont_write_bytecode = True

spec = importlib.util.spec_from_file_location("client", Path(__file__).with_name("extract-website-data.py"))
client = importlib.util.module_from_spec(spec)
spec.loader.exec_module(client)

SUCCESS = {"ok": True, "data": [{"title": "Fixture book", "price": "£12.50"}], "returned": 1, "total": 1,
           "truncated": False, "cursor": None, "requestId": "fixture-request", "meta": {"credits": {"total": 1}}}


class FixtureHandler(BaseHTTPRequestHandler):
    def log_message(self, *_):
        pass

    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length", "0")))
        self.server.received.append((self.path, dict(self.headers), json.loads(body)))
        response = self.server.payload
        self.send_response(self.server.status)
        self.send_header("Content-Type", "application/json")
        for key, value in self.server.extra_headers.items():
            self.send_header(key, value)
        self.end_headers()
        self.wfile.write(response if isinstance(response, bytes) else json.dumps(response).encode())

    def do_GET(self):
        self.server.redirect_visits += 1
        self.send_response(200)
        self.end_headers()


class ExtractionTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.server = ThreadingHTTPServer(("127.0.0.1", 0), FixtureHandler)
        cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
        cls.thread.start()
        cls.base = f"http://127.0.0.1:{cls.server.server_port}"

    @classmethod
    def tearDownClass(cls):
        cls.server.shutdown()
        cls.thread.join()
        cls.server.server_close()

    def setUp(self):
        self.server.payload = deepcopy(SUCCESS)
        self.server.status = 200
        self.server.extra_headers = {}
        self.server.received = []
        self.server.redirect_visits = 0

    def call(self, **kwargs):
        return client.extract(kwargs.get("base", self.base), "fixture-not-a-real-key", "https://books.toscrape.com/", "fixture-operation", timeout=2)

    def test_success_and_wire_contract(self):
        result = self.call()
        path, headers, body = self.server.received[0]
        self.assertEqual(path, "/v1/scrape")
        self.assertEqual(headers["Authorization"], "Bearer fixture-not-a-real-key")
        self.assertEqual(headers["Idempotency-Key"], "fixture-operation")
        self.assertEqual(body, {"url": "https://books.toscrape.com/", "fields": ["title", "price"], "maxTokens": 2000, "egress": "direct"})
        self.assertEqual(result["rows"][0]["price"], "£12.50")
        self.assertEqual(result["receipt"], SUCCESS["meta"])
        self.assertEqual(result["requestId"], "fixture-request")
        self.assertIn("+00:00", result["retrievedAt"])

    def test_empty_result_is_not_invented(self):
        self.server.payload["data"] = []
        self.assertEqual(self.call()["rows"], [])

    def test_invalid_records_are_rejected(self):
        for rows in ({"title": "Wrong shape"}, [{"title": "No price"}], [{"title": "", "price": "£1"}], [{"title": "Book", "price": 12.50}]):
            with self.subTest(rows=rows):
                self.server.payload["data"] = rows
                with self.assertRaises(client.ExtractionError):
                    self.call()

    def test_truncation_or_missing_flag_is_rejected(self):
        for flag in (True, None):
            self.server.payload["truncated"] = flag
            self.server.payload["cursor"] = "4"
            with self.assertRaisesRegex(client.ExtractionError, "incomplete"):
                self.call()

    def test_rate_limit_has_guidance_and_no_retry(self):
        self.server.status = 429
        self.server.extra_headers = {"Retry-After": "5"}
        self.server.payload = {"ok": False, "error": {"reason": "rate_limited"}, "requestId": "fixture-limit"}
        with self.assertRaisesRegex(client.ExtractionError, "reason=rate_limited; requestId=fixture-limit; retryAfter=5"):
            self.call()
        self.assertEqual(len(self.server.received), 1)

    def test_failure_envelope_is_rejected(self):
        self.server.payload = {"ok": False, "error": {"reason": "needs_residential"}}
        with self.assertRaisesRegex(client.ExtractionError, "needs_residential"):
            self.call()

    def test_non_json_success_and_error_responses(self):
        for status in (200, 502):
            with self.subTest(status=status):
                self.server.status = status
                self.server.payload = b"<html>gateway unavailable</html>"
                with self.assertRaises(client.ExtractionError) as caught:
                    self.call()
                self.assertNotIn("<html>", str(caught.exception))

    def test_redirect_does_not_forward_credential(self):
        self.server.status = 302
        self.server.extra_headers = {"Location": self.base + "/redirect-target"}
        self.server.payload = {}
        with self.assertRaisesRegex(client.ExtractionError, "HTTP 302"):
            self.call()
        self.assertEqual(self.server.redirect_visits, 0)
        self.assertEqual(len(self.server.received), 1)

    def test_unencrypted_remote_origin_rejected_before_network(self):
        with self.assertRaisesRegex(client.ExtractionError, "HTTPS"):
            self.call(base="http://api.example.com")
        self.assertEqual(self.server.received, [])

    def test_network_failure_has_no_automatic_retry(self):
        with patch.object(client, "build_opener") as build:
            build.return_value.open.side_effect = URLError("fixture timeout")
            with self.assertRaisesRegex(client.ExtractionError, "outcome may be unknown"):
                self.call()
            build.return_value.open.assert_called_once()

    def test_explicit_retry_preserves_body_and_operation_key(self):
        self.call()
        self.call()
        self.assertEqual(self.server.received[0], self.server.received[1])


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