"""Unit-Tests für die gemeinsame Python-HTTP- und CLI-Schicht."""

from __future__ import annotations

import argparse
import io
import json
import os
import sys
import tempfile
import unittest
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path
from unittest.mock import MagicMock, patch
from urllib.error import HTTPError, URLError

ROOT = Path(__file__).resolve().parents[2]
TOOLS = ROOT / "tools" / "python"
sys.path.insert(0, str(TOOLS))

import common  # noqa: E402


class FakeResponse:
    def __init__(self, status=200, body=b"", content_type="application/json"):
        self.status = status
        self._body = body
        self.headers = {"Content-Type": content_type}

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        return False

    def read(self):
        return self._body


class CommonTests(unittest.TestCase):
    def test_environment_credentials_are_trimmed_and_require_both_values(self):
        with patch.dict(os.environ, {common.API_USER_ENV: " user ", common.API_PASSWORD_ENV: " secret "}, clear=True):
            self.assertEqual(common.credentials_from_environment(), common.Credentials("user", "secret"))
        with patch.dict(os.environ, {common.API_USER_ENV: "user", common.API_PASSWORD_ENV: "   "}, clear=True):
            self.assertIsNone(common.credentials_from_environment())

    def test_prompt_credentials_prefers_environment_then_interactive_values(self):
        client = common.ApiClient("http://localhost:47145")
        with patch.dict(os.environ, {common.API_USER_ENV: " env-user ", common.API_PASSWORD_ENV: " env-pass "}, clear=True):
            client.prompt_credentials()
        self.assertEqual(client.credentials, common.Credentials("env-user", "env-pass"))

        client = common.ApiClient("http://localhost:47145", "default", "old")
        with patch.dict(os.environ, {}, clear=True), patch("builtins.input", return_value=""), patch(
            "getpass.getpass", return_value=" new-pass "
        ):
            client.prompt_credentials()
        self.assertEqual(client.credentials, common.Credentials("default", "new-pass"))

        with patch.dict(os.environ, {}, clear=True), patch("builtins.input", return_value=""):
            with self.assertRaises(common.ApiError):
                common.ApiClient("http://localhost:47145").prompt_credentials()
        with patch.dict(os.environ, {}, clear=True), patch("builtins.input", return_value="user"), patch(
            "getpass.getpass", return_value="   "
        ):
            with self.assertRaises(common.ApiError):
                common.ApiClient("http://localhost:47145").prompt_credentials()

    def test_payload_generation_and_multipart_encoding(self):
        data, headers = common._request_payload({"ä": "ö"}, None, None)
        self.assertEqual(json.loads(data.decode("utf-8")), {"ä": "ö"})
        self.assertEqual(headers["Content-Type"], common.APPLICATION_JSON)

        data, headers = common._request_payload(None, {"value": 7}, None)
        self.assertEqual(json.loads(data), {"value": 7})
        self.assertEqual(headers["Accept"], common.APPLICATION_JSON)

        data, headers = common._request_payload(None, None, None)
        self.assertIsNone(data)
        self.assertNotIn("Content-Type", headers)

        with tempfile.TemporaryDirectory() as directory:
            file_path = Path(directory) / 'a"b.txt'
            file_path.write_bytes(b"content")
            data, headers = common._request_payload(None, {"skip": None, "caption": "Text"}, {"file": file_path})
            self.assertIn(b'filename="a_b.txt"', data)
            self.assertIn(b'name="caption"', data)
            self.assertNotIn(b'name="skip"', data)
            self.assertTrue(headers["Content-Type"].startswith("multipart/form-data; boundary="))

            with self.assertRaises(FileNotFoundError):
                common.encode_multipart({}, {"file": Path(directory) / "missing.bin"})

    def test_response_parsing_and_error_decoding(self):
        self.assertIsNone(common.parse_json(b""))
        self.assertEqual(common.parse_json(b'{"ok":true}'), {"ok": True})
        self.assertIsNone(common.parse_json(b"\xff"))
        self.assertIsNone(common.parse_json(b"{"))

        self.assertIsNone(common.parse_response(b"", common.APPLICATION_JSON))
        self.assertEqual(common.parse_response(b'{"ok":true}', "application/problem+json"), {"ok": True})
        self.assertEqual(common.parse_response(b"plain", "text/plain"), "plain")
        self.assertEqual(common.parse_response(b"not-json", common.APPLICATION_JSON), "not-json")

        self.assertEqual(common.decode_error(b'{"message":"bad"}'), "bad")
        self.assertEqual(common.decode_error(b'{"error":"wrong"}'), "wrong")
        self.assertIn("value", common.decode_error(b'{"value":1}'))
        self.assertEqual(common.decode_error(b" text "), "text")
        self.assertEqual(common.decode_error(b""), "Unbekannter API-Fehler")

    def test_client_request_success_retry_and_failures(self):
        anonymous = common.ApiClient("http://localhost:47145")
        self.assertIsNone(anonymous._authorization_header())
        self.assertEqual(anonymous._request_headers({"Accept": common.APPLICATION_JSON}), {"Accept": common.APPLICATION_JSON})

        client = common.ApiClient("http://localhost:47145", " user ", " pass ")
        self.assertEqual(client.base_url, "http://localhost:47145/")
        self.assertTrue(client._authorization_header().startswith("Basic "))
        self.assertIn("Authorization", client._request_headers({"Accept": common.APPLICATION_JSON}))

        with patch.object(common, "urlopen", return_value=FakeResponse(200, b'{"ok":true}')):
            self.assertEqual(client.request("GET", "/api/health"), {"ok": True})

        with patch.object(common, "urlopen", return_value=FakeResponse(204, b"", "text/plain")):
            self.assertIsNone(client.request("DELETE", "/api/item", expected=(204,)))

        with patch.object(common, "urlopen", return_value=FakeResponse(202, b'{"queued":true}')):
            with self.assertRaises(common.ApiError) as context:
                client.request("POST", "/api/item")
        self.assertEqual(context.exception.status, 202)

        unauthorized = HTTPError(
            "http://localhost:47145/api/item",
            401,
            "Unauthorized",
            {},
            io.BytesIO(b'{"message":"login"}'),
        )
        with patch.object(common, "urlopen", side_effect=[unauthorized, FakeResponse(200, b'{"ok":true}')]), patch.object(
            client, "prompt_credentials"
        ) as prompt:
            self.assertEqual(client.request("POST", "/api/item"), {"ok": True})
        prompt.assert_called_once_with()

        forbidden = HTTPError(
            "http://localhost:47145/api/item",
            403,
            "Forbidden",
            {},
            io.BytesIO(b'{"error":"denied"}'),
        )
        with patch.object(common, "urlopen", side_effect=forbidden):
            with self.assertRaises(common.ApiError) as context:
                client.request("POST", "/api/item")
        self.assertEqual(context.exception.status, 403)
        self.assertEqual(context.exception.payload, {"error": "denied"})

        with patch.object(common, "urlopen", side_effect=URLError("offline")):
            with self.assertRaisesRegex(RuntimeError, "offline"):
                client.request("GET", "/api/health")

    def test_argument_and_prompt_helpers(self):
        parser = argparse.ArgumentParser()
        common.add_common_arguments(parser)
        args = parser.parse_args(["--base-url", "http://example.test", "--username", "alice"])
        client = common.client_from_args(args)
        self.assertEqual(client.base_url, "http://example.test/")
        self.assertIsNone(client.credentials)

        env_args = argparse.Namespace(base_url=None, username=None)
        with patch.dict(os.environ, {"WEGWICHTEL_URL": "http://env.test"}, clear=True):
            self.assertEqual(common.client_from_args(env_args).base_url, "http://env.test/")
        with patch.dict(os.environ, {}, clear=True):
            self.assertEqual(common.client_from_args(env_args).base_url, common.DEFAULT_BASE_URL + "/")

        self.assertEqual(common.ask("Zahl", 4, cast=int), 4)
        stderr = io.StringIO()
        with patch("builtins.input", side_effect=["bad", "5"]), redirect_stderr(stderr):
            self.assertEqual(common.ask("Zahl", required=True, cast=int), 5)
        self.assertIn("Ungültiger Wert", stderr.getvalue())
        with patch("builtins.input", return_value=""):
            self.assertIsNone(common.ask("Optional"))
        with patch("builtins.input", side_effect=["", "value"]), redirect_stderr(io.StringIO()):
            self.assertEqual(common.ask("Pflicht", required=True), "value")

        with patch("builtins.input", return_value=""):
            self.assertEqual(common.ask_with_default("Wert", 3, int), 3)
        with patch("builtins.input", side_effect=["bad", "4"]), redirect_stderr(io.StringIO()):
            self.assertEqual(common.ask_with_default("Wert", 3, int), 4)

    def test_path_confirmation_output_and_run_error_tracebacks(self):
        with tempfile.TemporaryDirectory() as directory:
            existing = Path(directory) / "file.txt"
            existing.write_text("ok", encoding="utf-8")
            self.assertEqual(common.ask_path("Datei", str(existing)), existing.resolve())
            with self.assertRaises(FileNotFoundError):
                common.ask_path("Datei", str(Path(directory) / "missing"))
            with patch("builtins.input", return_value=""):
                self.assertIsNone(common.ask_path("Optional", required=False))
            with patch("builtins.input", side_effect=["", str(Path(directory) / "missing"), str(existing)]), redirect_stderr(
                io.StringIO()
            ):
                self.assertEqual(common.ask_path("Pflicht"), existing.resolve())

        with patch("builtins.input", return_value="Ja"):
            self.assertTrue(common.confirm("Fortfahren?"))
        with patch("builtins.input", return_value="nein"):
            self.assertFalse(common.confirm("Fortfahren?"))

        stdout = io.StringIO()
        with redirect_stdout(stdout):
            common.print_result({"ok": True})
            common.print_result("text")
        self.assertIn('"ok": true', stdout.getvalue())
        self.assertTrue(stdout.getvalue().rstrip().endswith("text"))

        stdout = io.StringIO()
        with redirect_stdout(stdout):
            common.run(lambda: {"ok": True})
        self.assertIn('"ok": true', stdout.getvalue())

        stderr = io.StringIO()
        with redirect_stderr(stderr), self.assertRaises(SystemExit) as context:
            common.run(lambda: (_ for _ in ()).throw(ValueError("kaputt")))
        self.assertEqual(context.exception.code, 1)
        self.assertIn("Traceback", stderr.getvalue())
        self.assertIn("ValueError: kaputt", stderr.getvalue())

        stderr = io.StringIO()
        with redirect_stderr(stderr), self.assertRaises(SystemExit) as context:
            common.run(lambda: (_ for _ in ()).throw(KeyboardInterrupt()))
        self.assertEqual(context.exception.code, 130)
        self.assertIn("KeyboardInterrupt", stderr.getvalue())


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