# -*- coding: utf-8 -*-
# pylint: disable=redefined-outer-name
# -*- coding: utf-8 -*-
"""Auth=true real-link integration tests — Sprint 3.4-A.

Spawns a dedicated subprocess with QWENPAW_AUTH_ENABLED=true and
seeded credentials, then exercises:
  A1 GET /api/auth/status reflects auth enabled + has_users
  A2 POST /api/auth/login with correct credentials returns token
  A3 POST /api/auth/login with wrong password returns 401
  A4 Protected endpoint without token returns 401
  A5 Protected endpoint with valid Bearer token returns 200
"""
from __future__ import annotations

import os
import shutil
import socket
import subprocess
import sys
import threading
import time
from dataclasses import dataclass
from typing import Iterator

import httpx
import pytest
from helpers import default_http_timeout


_HTTP_TIMEOUT = default_http_timeout(15.0)
_AUTH_USERNAME = "integ-admin"
_AUTH_PASSWORD = "integ-pass-12345"


@dataclass
class _AuthAppServer:
    host: str
    port: int
    client: httpx.Client
    logs: list[str]

    @property
    def base_url(self) -> str:
        return f"http://{self.host}:{self.port}"

    def get(self, path: str, **kwargs):
        return self.client.get(f"{self.base_url}{path}", **kwargs)

    def post(self, path: str, **kwargs):
        return self.client.post(f"{self.base_url}{path}", **kwargs)


def _find_free_port(host: str = "127.0.0.1") -> int:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.bind((host, 0))
        sock.listen(1)
        return sock.getsockname()[1]


def _tee(stream, buf: list[str]) -> None:
    for line in stream:
        buf.append(line)


@pytest.fixture(scope="module")
def auth_app_server(  # pylint: disable=too-many-statements
    tmp_path_factory,
) -> Iterator[_AuthAppServer]:
    """Spawn a qwenpaw app subprocess with auth=true + seeded user."""
    tmp_path = tmp_path_factory.mktemp("auth_app_server")
    host = "127.0.0.1"
    port = _find_free_port(host)

    working_dir = tmp_path / "working"
    secret_dir = tmp_path / "working.secret"
    backups_dir = tmp_path / "working.backups"
    working_dir.mkdir(parents=True, exist_ok=True)
    secret_dir.mkdir(parents=True, exist_ok=True)
    backups_dir.mkdir(parents=True, exist_ok=True)

    env = os.environ.copy()
    for key in (
        "OPENAI_API_KEY",
        "ANTHROPIC_API_KEY",
        "DASHSCOPE_API_KEY",
    ):
        env.pop(key, None)
    env["QWENPAW_WORKING_DIR"] = str(working_dir)
    env["QWENPAW_SECRET_DIR"] = str(secret_dir)
    env["QWENPAW_BACKUP_DIR"] = str(backups_dir)
    env["QWENPAW_AUTH_ENABLED"] = "true"
    env["QWENPAW_AUTH_USERNAME"] = _AUTH_USERNAME
    env["QWENPAW_AUTH_PASSWORD"] = _AUTH_PASSWORD
    env["QWENPAW_UPLOAD_MAX_SIZE_MB"] = "10"
    env["NO_PROXY"] = "*"
    env["PYTHONUNBUFFERED"] = "1"
    env["PYTHONIOENCODING"] = "utf-8"

    logs: list[str] = []
    with subprocess.Popen(
        [
            sys.executable,
            "-m",
            "qwenpaw",
            "app",
            "--host",
            host,
            "--port",
            str(port),
            "--log-level",
            "info",
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        bufsize=1,
        encoding="utf-8",
        errors="replace",
        env=env,
    ) as process:
        assert process.stdout is not None
        log_thread = threading.Thread(
            target=_tee,
            args=(process.stdout, logs),
            daemon=True,
        )
        log_thread.start()

        client = httpx.Client(timeout=_HTTP_TIMEOUT, trust_env=False)
        try:
            deadline = time.time() + 60.0
            ready = False
            while time.time() < deadline:
                if process.poll() is not None:
                    raise AssertionError(
                        "qwenpaw app exited during startup\n"
                        f"logs:\n{''.join(logs)[-3000:]}",
                    )
                try:
                    r = client.get(
                        f"http://{host}:{port}/api/version",
                    )
                    if r.status_code == 200:
                        ready = True
                        break
                except (httpx.ConnectError, httpx.TimeoutException):
                    pass
                time.sleep(0.5)
            if not ready:
                raise AssertionError(
                    f"app not ready in time:\n" f"{''.join(logs)[-3000:]}",
                )

            yield _AuthAppServer(
                host=host,
                port=port,
                client=client,
                logs=logs,
            )
        finally:
            client.close()
            try:
                if sys.platform != "win32":
                    process.send_signal(2)  # SIGINT
                else:
                    process.terminate()
                process.wait(timeout=15)
            except Exception:
                process.kill()
                process.wait(timeout=5)
            shutil.rmtree(tmp_path, ignore_errors=True)


# ------------------------------------------------------------------ #
# A. Auth=true tests
# ------------------------------------------------------------------ #


@pytest.mark.integration
@pytest.mark.p1
def test_auth_status_reports_enabled_after_auto_register(
    auth_app_server,
) -> None:
    """Test purpose:
    - Verify GET /api/auth/status reports auth enabled and has_users=true
      after auto_register_from_env() seeds the credentials.

    API endpoints:
    - GET /api/auth/status (public)
    """
    resp = auth_app_server.get(
        "/api/auth/status",
        timeout=_HTTP_TIMEOUT,
    )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert body.get("enabled") is True, body
    assert body.get("has_users") is True, body


@pytest.mark.integration
@pytest.mark.p0
def test_auth_login_success_returns_token(auth_app_server) -> None:
    """Test purpose:
    - Verify POST /api/auth/login with correct credentials returns a
      non-empty token.

    API endpoints:
    - POST /api/auth/login (public)
    """
    resp = auth_app_server.post(
        "/api/auth/login",
        json={
            "username": _AUTH_USERNAME,
            "password": _AUTH_PASSWORD,
        },
        timeout=_HTTP_TIMEOUT,
    )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    token = body.get("token")
    assert isinstance(token, str) and token, body
    assert body.get("username") == _AUTH_USERNAME, body


@pytest.mark.integration
@pytest.mark.p1
def test_auth_login_wrong_password_returns_401(auth_app_server) -> None:
    """Test purpose:
    - Verify POST /api/auth/login with wrong password returns 401.

    API endpoints:
    - POST /api/auth/login
    """
    resp = auth_app_server.post(
        "/api/auth/login",
        json={
            "username": _AUTH_USERNAME,
            "password": "definitely-wrong",
        },
        timeout=_HTTP_TIMEOUT,
    )
    assert resp.status_code == 401, resp.text


@pytest.mark.integration
@pytest.mark.p0
def test_protected_endpoint_without_token_returns_401(
    auth_app_server,
) -> None:
    """Test purpose:
    - Verify GET /api/agents (protected) returns 401 without a token.
      Use X-Forwarded-For to simulate a non-localhost client (default
      allow_no_auth_hosts whitelists 127.0.0.1/::1).

    API endpoints:
    - GET /api/agents
    """
    resp = auth_app_server.get(
        "/api/agents",
        headers={"X-Forwarded-For": "203.0.113.7"},
        timeout=_HTTP_TIMEOUT,
    )
    assert resp.status_code == 401, resp.text


@pytest.mark.integration
@pytest.mark.p0
def test_protected_endpoint_with_valid_token_returns_200(
    auth_app_server,
) -> None:
    """Test purpose:
    - Verify GET /api/agents with a valid Bearer token returns 200.
      Uses X-Forwarded-For so the no-auth-hosts whitelist does not
      mask the test (otherwise localhost would pass without a token).

    Test flow:
    1. POST /api/auth/login → obtain token.
    2. GET /api/agents with Authorization: Bearer <token> → 200.

    API endpoints:
    - POST /api/auth/login
    - GET  /api/agents
    """
    login = auth_app_server.post(
        "/api/auth/login",
        json={
            "username": _AUTH_USERNAME,
            "password": _AUTH_PASSWORD,
        },
        timeout=_HTTP_TIMEOUT,
    )
    assert login.status_code == 200, login.text
    token = login.json()["token"]

    resp = auth_app_server.get(
        "/api/agents",
        headers={
            "Authorization": f"Bearer {token}",
            "X-Forwarded-For": "203.0.113.7",
        },
        timeout=_HTTP_TIMEOUT,
    )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    agents = body if isinstance(body, list) else body.get("agents", [])
    assert isinstance(agents, list), body
