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, List
from urllib.parse import parse_qs, urlparse
from unittest.mock import patch

from src.cli import main
from src.errors import UserError
from src.pipedrive.mapping import resolve_mapping

TEST_TOKEN = "test-token"

STUB_DEAL_FIELDS = [
    {"key": "cf_event_date", "name": "Event date"},
    {"key": "cf_venue", "name": "Venue"},
    {"key": "cf_lat", "name": "Lat"},
    {"key": "cf_lon", "name": "Lon"},
]

STUB_DEALS_PAGES = {
    0: [
        {
            "id": 100,
            "title": "In range",
            "cf_event_date": "2026-01-15",
            "cf_venue": "Venue A",
            "cf_lat": "52.1",
            "cf_lon": "4.9",
        },
        {
            "id": 200,
            "title": "Out of range",
            "cf_event_date": "2026-02-01",
            "cf_venue": "Venue B",
            "cf_lat": "52.2",
            "cf_lon": "5.0",
        },
    ],
    2: [],
}


class _PipedriveStubHandler(BaseHTTPRequestHandler):
    deal_fields: List[Dict[str, Any]] = []
    deals_pages: Dict[int, List[Dict[str, Any]]] = {}

    def do_GET(self) -> None:  # noqa: N802 - required by http.server
        parsed = urlparse(self.path)
        qs = parse_qs(parsed.query)
        token = (qs.get("api_token") or [None])[0]
        if token != TEST_TOKEN:
            self._send_json(401, {"success": False, "error": "unauthorized"})
            return

        if parsed.path.endswith("/dealFields"):
            self._send_json(
                200,
                {
                    "success": True,
                    "data": list(self.deal_fields),
                    "additional_data": {"pagination": {"more_items_in_collection": False}},
                },
            )
            return

        if parsed.path.endswith("/deals"):
            start = int((qs.get("start") or ["0"])[0])
            data = self.deals_pages.get(start, [])
            more = start == 0 and 2 in self.deals_pages
            payload: Dict[str, Any] = {
                "success": True,
                "data": list(data),
                "additional_data": {
                    "pagination": {
                        "more_items_in_collection": more,
                        "next_start": 2 if more else None,
                    }
                },
            }
            self._send_json(200, payload)
            return

        self._send_json(404, {"success": False, "error": "not found"})

    def _send_json(self, status: int, payload: Any) -> None:
        body = json.dumps(payload, ensure_ascii=True).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    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}/v1"
    finally:
        server.shutdown()
        server.server_close()
        thread.join(timeout=5)


def _write_mapping_file(path: Path) -> None:
    path.write_text(
        json.dumps(
            {
                "schema_version": 1,
                "fields": {
                    "event_date": {"field_name": "Event date"},
                    "venue_name": {"field_name": "Venue"},
                    "lat": {"field_name": "Lat"},
                    "lon": {"field_name": "Lon"},
                },
            }
        ),
        encoding="utf-8",
    )


class TestPipedrive(unittest.TestCase):
    def test_cli_fails_closed_without_token(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            mapping_path = Path(tmp) / "mapping.json"
            _write_mapping_file(mapping_path)
            with patch.dict(os.environ, {"PIPEDRIVE_FIELD_MAPPING_PATH": str(mapping_path)}, clear=True):
                with contextlib.redirect_stderr(io.StringIO()):
                    code = main(["pipedrive", "validate-mapping"])
        self.assertEqual(code, 2)

    def test_resolve_mapping_errors_on_ambiguous_field_name(self) -> None:
        mapping_data = {
            "schema_version": 1,
            "fields": {
                "event_date": {"field_key": "cf_event"},
                "venue_name": {"field_key": "cf_venue"},
                "lat": {"field_name": "Lat"},
                "lon": {"field_key": "cf_lon"},
            },
        }
        deal_fields = [
            {"key": "a", "name": "Lat"},
            {"key": "b", "name": "Lat"},
            {"key": "cf_event", "name": "Event date"},
            {"key": "cf_venue", "name": "Venue"},
            {"key": "cf_lon", "name": "Lon"},
        ]
        with self.assertRaises(UserError) as ctx:
            resolve_mapping(mapping_data, deal_fields)
        self.assertIn("Ambiguous", str(ctx.exception))

    def test_cli_list_deals_filters_by_date_range(self) -> None:
        _PipedriveStubHandler.deal_fields = list(STUB_DEAL_FIELDS)
        _PipedriveStubHandler.deals_pages = dict(STUB_DEALS_PAGES)

        with _run_stub_server(_PipedriveStubHandler) as base_url:
            with tempfile.TemporaryDirectory() as tmp:
                mapping_path = Path(tmp) / "mapping.json"
                _write_mapping_file(mapping_path)
                env = {
                    "PIPE_DRIVE_API_TOKEN": TEST_TOKEN,
                    "PIPEDRIVE_BASE_URL": base_url,
                    "PIPEDRIVE_ALLOW_INSECURE_HTTP": "1",
                    "PIPEDRIVE_FIELD_MAPPING_PATH": str(mapping_path),
                }
                stdout = io.StringIO()
                with patch.dict(os.environ, env, clear=True), contextlib.redirect_stdout(stdout):
                    code = main(["pipedrive", "list-deals", "--from", "2026-01-01", "--to", "2026-01-31"])

        self.assertEqual(code, 0)
        lines = [json.loads(line) for line in stdout.getvalue().splitlines() if line.strip()]
        self.assertEqual(len(lines), 1)
        self.assertEqual(lines[0]["deal_id"], 100)
        self.assertEqual(lines[0]["event_date"], "2026-01-15")
        self.assertEqual(lines[0]["lat"], 52.1)
        self.assertEqual(lines[0]["lon"], 4.9)
