"""Real curl/TLS reads: recover partial GETs, enforce deadlines and preserve the pin."""
from contextlib import ExitStack, redirect_stdout
import hashlib
import io
import json
import os
from pathlib import Path
import tempfile
import time
import unittest
from unittest import mock

import crashlytics_bootstrap as bootstrap
from crashlytics_bootstrap_test_support import running

BODY = b"verified fixture dependency"


class BootstrapTests(unittest.TestCase):
    def setUp(self):
        self.stack = ExitStack()
        self.addCleanup(self.stack.close)
        self.root = Path(self.stack.enter_context(tempfile.TemporaryDirectory()))
        self.server, self.certificate = self.stack.enter_context(running())
        self.server.expected_size = len(BODY)
        self.server.responses = [(200, BODY, 0)]
        self.stack.enter_context(mock.patch.dict(os.environ, {
            "RUNNER_TOOL_CACHE": str(self.root), "CURL_CA_BUNDLE": self.certificate,
            "HTTPS_PROXY": "", "https_proxy": "", "HTTP_PROXY": "", "http_proxy": ""}))
        for name, value in {"URL": self.server.url("/buildtools.jar"), "SIZE": len(BODY),
                            "SHA256": hashlib.sha256(BODY).hexdigest(),
                            "ATTEMPT_SECONDS": 1, "TOTAL_SECONDS": 5}.items():
            self.stack.enter_context(mock.patch.object(bootstrap, name, value))

    def test_verified_cache_reuses_bytes_without_another_request(self):
        first = bootstrap.prepare()
        self.assertEqual(first.read_bytes(), BODY)
        self.assertEqual(bootstrap.prepare(), first)
        self.assertEqual(self.server.requests, [b"GET /buildtools.jar HTTP/1.1"])

    def test_partial_transfer_recovers_only_the_get_and_cleans_partial_files(self):
        self.server.responses = [(200, BODY[:3], 0), (200, BODY, 0)]
        target = bootstrap.prepare()
        self.assertEqual(target.read_bytes(), BODY)
        self.assertEqual(self.server.requests, [b"GET /buildtools.jar HTTP/1.1"] * 2)
        self.assertEqual(list(target.parent.iterdir()), [target])

    def test_corrupt_cache_is_replaced_only_after_verification(self):
        target = bootstrap.prepare()
        target.write_bytes(b"corrupted")
        self.server.responses = [(200, BODY, 0)]
        self.assertEqual(bootstrap.prepare().read_bytes(), BODY)
        self.assertEqual(len(self.server.requests), 2)

    def test_wrong_checksum_refuses_without_retry_or_cache_admission(self):
        self.server.responses = [(200, b"x" * len(BODY), 0)]
        with self.assertRaises(bootstrap.BootstrapError) as caught:
            bootstrap.prepare()
        self.assertEqual(caught.exception.receipt["reason"], "checksum_or_size_mismatch")
        self.assertEqual(len(self.server.requests), 1)
        self.assertFalse(list(self.root.rglob("*.jar")))

    def test_stalled_download_has_a_total_deadline_and_no_partial_cache(self):
        self.server.responses = [(200, BODY, 0.5)] * 3
        started = time.monotonic()
        with mock.patch.object(bootstrap, "ATTEMPT_SECONDS", 0.1), \
                mock.patch.object(bootstrap, "TOTAL_SECONDS", 0.3):
            with self.assertRaises(bootstrap.BootstrapError) as caught:
                bootstrap.prepare()
        self.assertEqual(caught.exception.receipt["reason"], "download_timeout")
        self.assertLess(time.monotonic() - started, 1.5)
        self.assertFalse(list(self.root.rglob("*.jar")))

    def test_http_refusal_is_terminal_and_receipt_never_contains_provider_body(self):
        self.server.responses = [(403, b"SYNTHETIC private body", 0)]
        with self.assertRaises(bootstrap.BootstrapError) as caught:
            bootstrap.prepare()
        output = io.StringIO()
        with redirect_stdout(output):
            caught.exception.annotate()
        text = output.getvalue()
        receipt = json.loads(text.split("::", 2)[2])
        self.assertEqual(receipt["http_status"], 403)
        self.assertEqual(receipt["attempts"], 1)
        self.assertNotIn("SYNTHETIC", text)
        self.assertNotIn("private body", text)

    def test_tls_refusal_is_terminal_and_sends_no_request(self):
        with mock.patch.dict(os.environ, {"CURL_CA_BUNDLE": ""}):
            with self.assertRaises(bootstrap.BootstrapError) as caught:
                bootstrap.prepare()
        self.assertEqual(caught.exception.receipt["reason"], "tls_refused")
        self.assertEqual(self.server.requests, [])


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