"""Real ASC helper and Requests preparation; only socket dispatch and sleeps are replaced."""
import contextlib
import io
import os
from pathlib import Path
import sys
import unittest
from unittest import mock

sys.path.insert(0, str(Path(__file__).resolve().parent))
import asc_common

PRIVATE = "SYNTHETIC_PRIVATE_MARKER"
# CI reaches App Store Connect directly: no proxy is configured and none is inherited.
ENV = {"APPLE_STORE_PROXY_URL": "", "HTTPS_PROXY": "", "https_proxy": "", "ALL_PROXY": "",
       "NO_PROXY": "", "no_proxy": "", "REQUESTS_CA_BUNDLE": "",
       "CURL_CA_BUNDLE": "", "ASC_REQUEST_TIMEOUT_CONNECT_SEC": "", "ASC_REQUEST_TIMEOUT_READ_SEC": ""}


class PrivateFailure(asc_common.requests.RequestException):
    def __str__(self):
        raise AssertionError("Exception text must never be inspected")


@mock.patch.dict(os.environ, ENV)
class NetworkDiagnosticTests(unittest.TestCase):
    def test_terminal_categories_preserve_transport_and_privacy(self):
        errors = asc_common.requests.exceptions
        cases = [(errors.SSLError, "tls"),
                 (errors.ConnectTimeout, "connect_timeout"), (errors.ReadTimeout, "read_timeout"),
                 (errors.Timeout, "timeout"), (errors.ConnectionError, "connection"),
                 (errors.RequestException, "request"), (PrivateFailure, "request")]
        for error_type, category in cases:
            with self.subTest(category=category, error_type=error_type.__name__):
                stderr, stdout = io.StringIO(), io.StringIO()
                with mock.patch.object(asc_common.requests.Session, "send", autospec=True,
                                       side_effect=error_type(PRIVATE)) as sender, \
                        mock.patch.object(asc_common.time, "sleep") as sleep, \
                        contextlib.redirect_stderr(stderr), contextlib.redirect_stdout(stdout):
                    with self.assertRaises(SystemExit) as caught:
                        asc_common.request("GET", "/builds", PRIVATE, params={"limit": "200"})
                self.assertEqual(sender.call_count, 3)
                self.assertEqual(sleep.call_args_list, [mock.call(2), mock.call(8)])
                self.assertEqual(stderr.getvalue().count(f"category={category}"), 2)
                self.assertIn(f"after 3 attempts (category={category})", str(caught.exception))
                self.assertNotIn(PRIVATE, stdout.getvalue() + stderr.getvalue() + str(caught.exception))
                self.assertEqual(stdout.getvalue(), "")
                for call in sender.call_args_list:
                    self.assertEqual(call.args[1].url, asc_common.ASC_BASE + "/builds?limit=200")
                    self.assertEqual(call.args[1].headers["Authorization"], f"Bearer {PRIVATE}")
                    # Apple is reached directly from CI; nothing may resolve a proxy for it.
                    self.assertEqual(call.kwargs.get("proxies") or {}, {})
                    self.assertEqual(call.kwargs["timeout"], (10.0, 30.0))
                    self.assertIs(call.kwargs["verify"], True)

    def test_transient_failure_returns_success_without_extra_sender_calls(self):
        response = asc_common.requests.Response()
        response.status_code = 200
        stderr = io.StringIO()
        with mock.patch.object(asc_common.requests.Session, "send", autospec=True,
                               side_effect=[asc_common.requests.exceptions.ConnectionError(PRIVATE), response]) as sender, \
                mock.patch.object(asc_common.time, "sleep") as sleep, contextlib.redirect_stderr(stderr):
            self.assertIs(asc_common.request("GET", "/builds", PRIVATE), response)
        self.assertEqual(sender.call_count, 2)
        sleep.assert_called_once_with(2)
        self.assertIn("category=connection", stderr.getvalue())
        self.assertNotIn(PRIVATE, stderr.getvalue())

    def test_single_attempt_does_not_sleep(self):
        with mock.patch.object(asc_common.requests.Session, "send", autospec=True,
                               side_effect=asc_common.requests.exceptions.ReadTimeout(PRIVATE)) as sender, \
                mock.patch.object(asc_common.time, "sleep") as sleep:
            with self.assertRaisesRegex(SystemExit, "after 1 attempts.*category=read_timeout"):
                asc_common.request("GET", "/builds", PRIVATE, max_attempts=1)
        self.assertEqual(sender.call_count, 1)
        sleep.assert_not_called()

    def test_http_refusal_is_not_reclassified_as_network_failure(self):
        response = asc_common.requests.Response()
        response.status_code = 401
        response._content = b"unauthorized"
        with mock.patch.object(asc_common.requests.Session, "send", return_value=response) as sender, \
                mock.patch.object(asc_common.time, "sleep") as sleep:
            with self.assertRaises(SystemExit) as caught:
                asc_common.request("GET", "/builds", PRIVATE)
        self.assertEqual(sender.call_count, 1)
        sleep.assert_not_called()
        self.assertEqual(str(caught.exception), "ASC GET /builds failed: 401\nunauthorized")


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