"""
Module: test_auth
Description: Tests for authentication

Implements:
    - docs/cli/reference.md#huitzo-login
"""

from pathlib import Path
from typing import Generator
from unittest.mock import MagicMock, patch

import httpx
import pytest
from typer.testing import CliRunner

from huitzo_cli.auth import AuthClient, TokenStore
from huitzo_cli.config import ConfigManager
from huitzo_cli.main import app


@pytest.fixture
def temp_token_file(temp_dir: Path) -> Generator[Path, None, None]:
    """Provide a temporary token file."""
    token_file = temp_dir / "token"
    with patch.object(TokenStore, "_get_token_path", return_value=token_file):
        yield token_file


def test_token_store_save(temp_token_file: Path) -> None:
    """Test saving token."""
    store = TokenStore()
    store.save("test-token")

    assert temp_token_file.exists()
    assert temp_token_file.read_text().strip() == "test-token"


def test_token_store_load(temp_token_file: Path) -> None:
    """Test loading token."""
    temp_token_file.parent.mkdir(parents=True, exist_ok=True)
    temp_token_file.write_text("test-token")

    store = TokenStore()
    assert store.load() == "test-token"


def test_token_store_load_missing() -> None:
    """Test loading when token doesn't exist."""
    store = TokenStore()
    # Mock the token path to a non-existent location
    with patch.object(TokenStore, "_get_token_path", return_value=Path("/nonexistent/token")):
        store = TokenStore()
        assert store.load() is None


def test_token_store_delete(temp_token_file: Path) -> None:
    """Test deleting token."""
    temp_token_file.parent.mkdir(parents=True, exist_ok=True)
    temp_token_file.write_text("test-token")

    store = TokenStore()
    store.delete()

    assert not temp_token_file.exists()


def test_auth_client_login(temp_dir: Path, temp_token_file: Path) -> None:
    """Test auth client login with token."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    auth_client.login(email="test@example.com", token="test-token")

    assert auth_client.is_authenticated()
    assert auth_client.get_token() == "test-token"


def test_auth_client_login_no_config_dualwrite(
    temp_dir: Path, temp_token_file: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test login does NOT write token to config (C2: single source of truth)."""
    config_dir = temp_dir / "config"
    monkeypatch.setattr(ConfigManager, "get_config_dir", staticmethod(lambda: config_dir))

    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    with patch.object(config, "set") as mock_set:
        auth_client.login(email="test@example.com", token="test-token")
        # config.set should NOT be called with auth.token
        for call_args in mock_set.call_args_list:
            assert call_args[0][0] != "auth.token"


def test_auth_client_logout(temp_dir: Path, temp_token_file: Path) -> None:
    """Test auth client logout."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    auth_client.login(email="test@example.com", token="test-token")
    assert auth_client.is_authenticated()

    auth_client.logout()
    assert not auth_client.is_authenticated()


def test_auth_client_get_token_from_file(temp_dir: Path, temp_token_file: Path) -> None:
    """Test getting token from token file (single source)."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    auth_client.login(email="test@example.com", token="test-token")
    assert auth_client.get_token() == "test-token"


def test_auth_client_get_token_from_env(
    temp_dir: Path, temp_token_file: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test HUITZO_AUTH_TOKEN env var takes priority."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    monkeypatch.setenv("HUITZO_AUTH_TOKEN", "env-token-123")
    assert auth_client.get_token() == "env-token-123"


def test_auth_client_get_token_returns_none(temp_dir: Path, temp_token_file: Path) -> None:
    """Test get_token returns None when no token file exists (no config fallback)."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    assert auth_client.get_token() is None


def test_auth_command_login_with_env_token(
    cli_runner: CliRunner, temp_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test login command with HUITZO_AUTH_TOKEN env var."""
    config_dir = temp_dir / "config"
    monkeypatch.setattr(ConfigManager, "get_config_dir", staticmethod(lambda: config_dir))

    token_file = temp_dir / "token"
    monkeypatch.setattr(TokenStore, "_get_token_path", staticmethod(lambda: token_file))

    monkeypatch.setenv("HUITZO_AUTH_TOKEN", "test-token")

    result = cli_runner.invoke(app, ["login"])

    assert result.exit_code == 0
    assert "Logged in with token from HUITZO_AUTH_TOKEN" in result.stdout


def test_auth_command_login_rejects_old_token_flag(
    cli_runner: CliRunner, temp_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test --token flag is removed from CLI surface (C6)."""
    config_dir = temp_dir / "config"
    monkeypatch.setattr(ConfigManager, "get_config_dir", staticmethod(lambda: config_dir))
    token_file = temp_dir / "token"
    monkeypatch.setattr(TokenStore, "_get_token_path", staticmethod(lambda: token_file))

    result = cli_runner.invoke(app, ["login", "--token", "test-token"])
    # Should fail because --token is no longer recognized
    assert result.exit_code != 0


def test_auth_command_login_prompts_for_credentials(cli_runner: CliRunner) -> None:
    """Test login command prompts for email and password when not provided."""
    result = cli_runner.invoke(app, ["login"], input="test@example.com\npassword\n")

    # Will fail because no backend is running, but should not show "not yet available"
    assert result.exit_code == 1
    assert "Login failed" in result.stdout


def test_auth_command_logout(
    cli_runner: CliRunner, temp_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test logout command."""
    config_dir = temp_dir / "config"
    monkeypatch.setattr(ConfigManager, "get_config_dir", staticmethod(lambda: config_dir))

    token_file = temp_dir / "token"
    token_file.parent.mkdir(parents=True, exist_ok=True)
    token_file.write_text("test-token")
    monkeypatch.setattr(TokenStore, "_get_token_path", staticmethod(lambda: token_file))

    result = cli_runner.invoke(app, ["logout"])

    assert result.exit_code == 0
    assert "Logged out successfully" in result.stdout


# --- HTTP login flow tests ---


def _mock_response(status_code: int, json_data: dict | None = None, text: str = "") -> MagicMock:
    """Create a mock httpx.Response."""
    resp = MagicMock(spec=httpx.Response)
    resp.status_code = status_code
    resp.text = text
    if json_data is not None:
        resp.json.return_value = json_data
    else:
        resp.json.side_effect = ValueError("No JSON")
    return resp


def test_auth_client_http_login_success(temp_dir: Path, temp_token_file: Path) -> None:
    """Test successful HTTP-based login stores token."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    mock_resp = _mock_response(
        200,
        {
            "data": {
                "tokens": {"accessToken": "abc123", "refreshToken": "r1", "expiresAt": 9999},
                "user": {"email": "user@test.com"},
            }
        },
    )

    with patch("huitzo_cli.auth.httpx.post", return_value=mock_resp):
        data = auth_client.login(
            email="user@test.com", password="secret", url="http://localhost:8000"
        )

    assert data["tokens"]["accessToken"] == "abc123"
    assert auth_client.get_token() == "abc123"


def test_auth_client_http_login_failure(temp_dir: Path, temp_token_file: Path) -> None:
    """Test HTTP login with bad credentials raises RuntimeError."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    mock_resp = _mock_response(
        401,
        {"error": {"message": "Invalid credentials"}},
    )

    with patch("huitzo_cli.auth.httpx.post", return_value=mock_resp):
        with pytest.raises(RuntimeError, match="Invalid credentials"):
            auth_client.login(email="user@test.com", password="wrong", url="http://localhost:8000")


def test_auth_client_http_login_network_error(temp_dir: Path, temp_token_file: Path) -> None:
    """Test HTTP login with network error raises RuntimeError."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    with patch(
        "huitzo_cli.auth.httpx.post",
        side_effect=httpx.ConnectError("Connection refused"),
    ):
        with pytest.raises(RuntimeError, match="Could not connect to backend"):
            auth_client.login(email="user@test.com", password="secret", url="http://localhost:8000")


def test_auth_client_http_login_rejects_http_url(temp_dir: Path, temp_token_file: Path) -> None:
    """Test login raises RuntimeError for non-HTTPS, non-localhost URL (W28)."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    with pytest.raises(RuntimeError, match="HTTPS required"):
        auth_client.login(
            email="user@test.com", password="secret", url="http://remote-server.example.com"
        )


def test_auth_client_http_login_allows_localhost(temp_dir: Path, temp_token_file: Path) -> None:
    """Test login allows http://localhost for development."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    mock_resp = _mock_response(
        200,
        {
            "data": {
                "tokens": {"accessToken": "abc123"},
            }
        },
    )

    with patch("huitzo_cli.auth.httpx.post", return_value=mock_resp):
        data = auth_client.login(
            email="user@test.com", password="secret", url="http://localhost:8000"
        )
    assert data["tokens"]["accessToken"] == "abc123"


def test_auth_client_http_login_malformed_response(temp_dir: Path, temp_token_file: Path) -> None:
    """Test HTTP login with non-JSON error response raises RuntimeError."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    mock_resp = _mock_response(500, text="Internal Server Error")

    with patch("huitzo_cli.auth.httpx.post", return_value=mock_resp):
        with pytest.raises(RuntimeError, match="HTTP 500"):
            auth_client.login(email="user@test.com", password="secret", url="http://localhost:8000")


def test_auth_client_login_requires_password_without_token(
    temp_dir: Path, temp_token_file: Path
) -> None:
    """Test that login without token and without password raises."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    with pytest.raises(RuntimeError, match="Password is required"):
        auth_client.login(email="user@test.com", url="http://localhost:8000")


# --- JWT expiry check tests ---


def _make_jwt(payload: dict, header: dict | None = None) -> str:
    """Build a fake JWT (header.payload.signature) for testing."""
    import base64
    import json

    header = header or {"alg": "HS256", "typ": "JWT"}
    h = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b"=").decode()
    p = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b"=").decode()
    return f"{h}.{p}.fake-signature"


def test_check_jwt_expiry_valid_token() -> None:
    """Non-expired JWT should return (False, '')."""
    import time

    token = _make_jwt({"sub": "user", "exp": time.time() + 3600})
    expired, msg = AuthClient._check_jwt_expiry(token)
    assert expired is False
    assert msg == ""


def test_check_jwt_expiry_expired_token() -> None:
    """Expired JWT should return (True, ...)."""
    import time

    token = _make_jwt({"sub": "user", "exp": time.time() - 100})
    expired, msg = AuthClient._check_jwt_expiry(token)
    assert expired is True
    assert "expired" in msg.lower()


def test_check_jwt_expiry_non_jwt() -> None:
    """Non-JWT strings should be treated as not expired."""
    expired, msg = AuthClient._check_jwt_expiry("not-a-jwt")
    assert expired is False
    assert msg == ""


def test_check_jwt_expiry_malformed_payload() -> None:
    """Malformed base64 payload should be treated as not expired."""
    expired, msg = AuthClient._check_jwt_expiry("header.!!!invalid!!!.sig")
    assert expired is False
    assert msg == ""


def test_check_jwt_expiry_no_exp_claim() -> None:
    """JWT without exp claim should be treated as not expired."""
    token = _make_jwt({"sub": "user"})
    expired, msg = AuthClient._check_jwt_expiry(token)
    assert expired is False
    assert msg == ""


# --- validate_token tests ---


def test_validate_token_no_token(temp_dir: Path, temp_token_file: Path) -> None:
    """validate_token returns failure when no token exists."""
    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    valid, msg = auth_client.validate_token()
    assert valid is False
    assert "huitzo login" in msg.lower()


def test_validate_token_expired(temp_dir: Path, temp_token_file: Path) -> None:
    """validate_token returns failure for expired JWT."""
    import time

    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    expired_jwt = _make_jwt({"sub": "user", "exp": time.time() - 100})
    TokenStore().save(expired_jwt)

    valid, msg = auth_client.validate_token()
    assert valid is False
    assert "expired" in msg.lower()


def test_validate_token_backend_rejects(temp_dir: Path, temp_token_file: Path) -> None:
    """validate_token returns failure when backend rejects token."""
    import time

    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    # Valid expiry but backend rejects
    token = _make_jwt({"sub": "user", "exp": time.time() + 3600})
    TokenStore().save(token)

    with patch.object(auth_client, "get_current_user", return_value=None):
        valid, msg = auth_client.validate_token()
    assert valid is False
    assert "invalid or expired" in msg.lower()


def test_validate_token_success(temp_dir: Path, temp_token_file: Path) -> None:
    """validate_token returns success when token is valid and backend accepts."""
    import time

    config = ConfigManager(project_root=temp_dir)
    auth_client = AuthClient(config)

    token = _make_jwt({"sub": "user", "exp": time.time() + 3600})
    TokenStore().save(token)

    with patch.object(auth_client, "get_current_user", return_value={"email": "u@t.com"}):
        valid, msg = auth_client.validate_token()
    assert valid is True
    assert msg == ""


# --- _remove_stale_config_token tests ---


def test_remove_stale_config_token_removes_key(
    temp_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Login should remove auth.token from config.yaml."""
    import yaml

    config_dir = temp_dir / "config"
    monkeypatch.setattr(ConfigManager, "get_config_dir", staticmethod(lambda: config_dir))
    config_dir.mkdir(parents=True, exist_ok=True)
    config_file = config_dir / "config.yaml"
    config_file.write_text(yaml.dump({"auth": {"token": "stale", "token_file": "/p"}}))

    from huitzo_cli.commands.auth import _remove_stale_config_token

    config = ConfigManager(project_root=temp_dir)
    _remove_stale_config_token(config)

    data = yaml.safe_load(config_file.read_text())
    assert "token" not in data["auth"]
    assert data["auth"]["token_file"] == "/p"


def test_remove_stale_config_token_noop_when_absent(
    temp_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """No-op when config.yaml has no auth.token."""
    import yaml

    config_dir = temp_dir / "config"
    monkeypatch.setattr(ConfigManager, "get_config_dir", staticmethod(lambda: config_dir))
    config_dir.mkdir(parents=True, exist_ok=True)
    config_file = config_dir / "config.yaml"
    original = {"api_url": "https://api.huitzo.ai"}
    config_file.write_text(yaml.dump(original))

    from huitzo_cli.commands.auth import _remove_stale_config_token

    config = ConfigManager(project_root=temp_dir)
    _remove_stale_config_token(config)

    data = yaml.safe_load(config_file.read_text())
    assert data == original


def test_remove_stale_config_token_noop_when_no_file(
    temp_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """No-op when config.yaml does not exist."""
    config_dir = temp_dir / "nonexistent"
    monkeypatch.setattr(ConfigManager, "get_config_dir", staticmethod(lambda: config_dir))

    from huitzo_cli.commands.auth import _remove_stale_config_token

    config = ConfigManager(project_root=temp_dir)
    _remove_stale_config_token(config)
    # Should not raise
