from __future__ import annotations

import contextlib
import io
import json
import os
import tempfile
import threading
import unittest
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from typing import Any, Dict
from unittest.mock import patch

from src.cli import main


DEAL_ID = 123
PLACE = "Amsterdam"
AUDIT_RUN_ID = "RUN1"


def _write_payload(out: Path) -> Path:
    payload_dir = out / "gbp" / "posts"
    payload_dir.mkdir(parents=True, exist_ok=True)
    path = payload_dir / f"deal_{DEAL_ID}.json"
    path.write_text(
        json.dumps(
            {
                "deal_id": DEAL_ID,
                "place": PLACE,
                "text": "Hello from test",
                "audit_run_id": AUDIT_RUN_ID,
                "photos": [],
            },
            sort_keys=True,
            ensure_ascii=True,
            indent=2,
        )
        + "\n",
        encoding="utf-8",
        newline="\n",
    )
    return path


class _GbpStubHandler(BaseHTTPRequestHandler):
    last_path: str | None = None
    last_auth: str | None = None
    last_body: Dict[str, Any] | None = None
    expected_token: str = ""

    def do_POST(self) -> None:  # noqa: N802 - required by http.server
        _GbpStubHandler.last_path = self.path
        _GbpStubHandler.last_auth = self.headers.get("Authorization")
        length = int(self.headers.get("Content-Length") or "0")
        body = self.rfile.read(length).decode("utf-8")
        _GbpStubHandler.last_body = json.loads(body) if body.strip() else {}

        if _GbpStubHandler.last_auth != f"Bearer {_GbpStubHandler.expected_token}":
            self.send_response(401)
            self.end_headers()
            self.wfile.write(b"unauthorized")
            return

        resp = json.dumps({"name": "localPosts/1"}, ensure_ascii=True).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(resp)))
        self.end_headers()
        self.wfile.write(resp)

    def log_message(self, format: str, *args: object) -> None:  # noqa: A002
        return


@contextlib.contextmanager
def _run_stub_server(handler_cls: type[BaseHTTPRequestHandler]) -> Any:
    server = HTTPServer(("127.0.0.1", 0), handler_cls)
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    host, port = server.server_address
    try:
        yield f"http://{host}:{port}"
    finally:
        server.shutdown()
        server.server_close()
        thread.join(timeout=5)


class TestGbpPublish(unittest.TestCase):
    def test_publish_fails_closed_without_location_map(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            out = Path(tmp) / "out"
            _write_payload(out)

            stderr = io.StringIO()
            with patch.dict(os.environ, {"OUTPUT_ROOT": str(out)}, clear=True), contextlib.redirect_stderr(stderr):
                code = main(["gbp", "publish", "--deal-id", str(DEAL_ID), "--confirm", "--dry-run", "false"])

            self.assertEqual(code, 2)
            self.assertIn("GBP_LOCATION_ID_MAP", stderr.getvalue())

    def test_publish_blocks_unmapped_location(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            out = Path(tmp) / "out"
            _write_payload(out)
            env = {
                "OUTPUT_ROOT": str(out),
                "GBP_LOCATION_ID_MAP": json.dumps({"utrecht": "LOC1"}),
                "GBP_ACCOUNT_ID": "A1",
                "GBP_ACCESS_TOKEN": "T1",
            }

            stderr = io.StringIO()
            with patch.dict(os.environ, env, clear=True), contextlib.redirect_stderr(stderr):
                code = main(["gbp", "publish", "--deal-id", str(DEAL_ID), "--confirm", "--dry-run", "false"])

            self.assertEqual(code, 2)
            self.assertIn("Unmapped GBP location", stderr.getvalue())

    def test_publish_requires_confirm_for_real_publish(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            out = Path(tmp) / "out"
            _write_payload(out)
            env = {
                "OUTPUT_ROOT": str(out),
                "GBP_LOCATION_ID_MAP": json.dumps({"amsterdam": "LOC1"}),
                "GBP_ACCOUNT_ID": "A1",
                "GBP_ACCESS_TOKEN": "T1",
            }

            stderr = io.StringIO()
            with patch.dict(os.environ, env, clear=True), contextlib.redirect_stderr(stderr):
                code = main(["gbp", "publish", "--deal-id", str(DEAL_ID), "--dry-run", "false"])

            self.assertEqual(code, 2)
            self.assertIn("--confirm", stderr.getvalue())

    def test_publish_dry_run_does_not_write_artifact(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            out = Path(tmp) / "out"
            _write_payload(out)
            env = {"OUTPUT_ROOT": str(out), "GBP_LOCATION_ID_MAP": json.dumps({"amsterdam": "LOC1"})}

            stdout = io.StringIO()
            with patch.dict(os.environ, env, clear=True), contextlib.redirect_stdout(stdout):
                code = main(["gbp", "publish", "--deal-id", str(DEAL_ID), "--dry-run", "true"])

            self.assertEqual(code, 0)
            self.assertFalse((out / "audit" / AUDIT_RUN_ID).exists())

    def test_publish_real_calls_http_and_writes_audit_result(self) -> None:
        _GbpStubHandler.expected_token = "T1"
        with _run_stub_server(_GbpStubHandler) as base_url, tempfile.TemporaryDirectory() as tmp:
            out = Path(tmp) / "out"
            _write_payload(out)
            env = {
                "OUTPUT_ROOT": str(out),
                "GBP_LOCATION_ID_MAP": json.dumps({"amsterdam": "LOC1"}),
                "GBP_ALLOW_INSECURE_HTTP": "1",
                "GBP_BASE_URL": base_url,
                "GBP_ACCOUNT_ID": "A1",
                "GBP_ACCESS_TOKEN": "T1",
            }

            stdout = io.StringIO()
            with patch.dict(os.environ, env, clear=True), contextlib.redirect_stdout(stdout):
                code = main(["gbp", "publish", "--deal-id", str(DEAL_ID), "--confirm", "--dry-run", "false"])

            self.assertEqual(code, 0)
            self.assertEqual(_GbpStubHandler.last_path, "/accounts/A1/locations/LOC1/localPosts")
            self.assertEqual(_GbpStubHandler.last_body, {"summary": "Hello from test"})
            self.assertTrue((out / "audit" / AUDIT_RUN_ID / f"gbp_publish_deal_{DEAL_ID}.json").exists())
