"""Tests for agnes auth login/logout/whoami commands."""

import json
import pytest
from unittest.mock import patch, MagicMock

from typer.testing import CliRunner
from cli.main import app

runner = CliRunner()


@pytest.fixture(autouse=True)
def tmp_config(tmp_path, monkeypatch):
    monkeypatch.setenv("AGNES_CONFIG_DIR", str(tmp_path / "config"))
    monkeypatch.setenv("AGNES_LOCAL_DIR", str(tmp_path / "local"))
    (tmp_path / "config").mkdir()
    (tmp_path / "local").mkdir()
    yield tmp_path


def _make_response(status_code=200, json_data=None, text=""):
    resp = MagicMock()
    resp.status_code = status_code
    resp.json.return_value = json_data or {}
    resp.text = text
    return resp


class TestAuthLogin:
    def test_login_success(self):
        """Login with valid credentials saves token and shows confirmation."""
        mock_resp = _make_response(200, {
            "access_token": "tok123",
            "email": "alice@example.com",
            "role": "user",
        })
        with patch("cli.commands.auth.api_post", return_value=mock_resp):
            with patch("cli.commands.auth.save_token") as mock_save:
                # Empty password (simulates magic-link / OAuth account) — still 200 from server
                result = runner.invoke(app, ["auth", "login", "--email", "alice@example.com"], input="\n")
        assert result.exit_code == 0
        assert "alice@example.com" in result.output
        mock_save.assert_called_once_with("tok123", "alice@example.com")

    def test_login_invalid_credentials(self):
        """Login with bad credentials exits with error."""
        mock_resp = _make_response(401, {"detail": "Invalid credentials"})
        with patch("cli.commands.auth.api_post", return_value=mock_resp):
            result = runner.invoke(app, ["auth", "login", "--email", "bad@example.com"], input="\n")
        assert result.exit_code == 1
        assert "Login failed" in result.output

    def test_login_connection_error(self):
        """Login propagates connection errors cleanly."""
        with patch("cli.commands.auth.api_post", side_effect=Exception("Connection refused")):
            result = runner.invoke(app, ["auth", "login", "--email", "alice@example.com"], input="\n")
        assert result.exit_code == 1
        assert "Connection error" in result.output


class TestAuthLogout:
    def test_logout(self):
        """Logout clears token and confirms."""
        with patch("cli.commands.auth.clear_token") as mock_clear:
            result = runner.invoke(app, ["auth", "logout"])
        assert result.exit_code == 0
        assert "Logged out" in result.output
        mock_clear.assert_called_once()


class TestAuthImportToken:
    def _make_jwt(self, email="alice@example.com", typ="pat"):
        import jwt as pyjwt
        return pyjwt.encode(
            {"email": email, "typ": typ, "sub": "u-1"},
            "unused",
            algorithm="HS256",
        )

    def _mock_verify(self, status_code=200, json_data=None):
        """Build a patcher for cli.commands.auth.httpx.Client that returns a canned response."""
        resp = _make_response(status_code, json_data or {})
        mock_client = MagicMock()
        mock_client.__enter__.return_value = mock_client
        mock_client.__exit__.return_value = False
        mock_client.get.return_value = resp
        return patch("cli.commands.auth.httpx.Client", return_value=mock_client)

    def test_import_token_success_writes_canonical_format(self, tmp_path, monkeypatch):
        """Valid JWT + 200 from server -> canonical token.json on disk."""
        monkeypatch.setenv("AGNES_SERVER", "http://example.test")
        token = self._make_jwt(email="bob@example.com")

        with self._mock_verify(200):
            result = runner.invoke(app, ["auth", "import-token", "--token", token])

        assert result.exit_code == 0, result.output
        assert "bob@example.com" in result.output

        token_file = tmp_path / "config" / "token.json"
        assert token_file.exists()
        data = json.loads(token_file.read_text())
        # v19: token.json no longer carries a role label (auth derives admin
        # from group memberships server-side).
        assert data == {"access_token": token, "email": "bob@example.com"}

    def test_import_token_401_does_not_overwrite_existing(self, tmp_path, monkeypatch):
        """A 401 response aborts import and leaves the prior token file untouched."""
        monkeypatch.setenv("AGNES_SERVER", "http://example.test")
        existing = {"access_token": "keep-me", "email": "old@example.com"}
        token_file = tmp_path / "config" / "token.json"
        token_file.write_text(json.dumps(existing))

        token = self._make_jwt()
        with self._mock_verify(401, {"detail": "Token revoked"}):
            result = runner.invoke(app, ["auth", "import-token", "--token", token])

        assert result.exit_code == 1
        assert "Token rejected by server" in result.output
        assert "Token revoked" in result.output
        # Existing file must be intact.
        assert json.loads(token_file.read_text()) == existing

    def test_import_token_with_server_flag_persists_server_to_config_yaml(
        self, tmp_path, monkeypatch
    ):
        """Passing --server should write `server: URL` to ~/.config/agnes/config.yaml
        so the user never has to configure the server in a separate step."""
        # No AGNES_SERVER env var — rely entirely on the --server flag for persistence.
        monkeypatch.delenv("AGNES_SERVER", raising=False)
        token = self._make_jwt(email="dave@example.com")

        with self._mock_verify(200):
            result = runner.invoke(
                app,
                [
                    "auth", "import-token",
                    "--token", token,
                    "--server", "https://agnes.example.com",
                ],
            )
        assert result.exit_code == 0, result.output

        config_file = tmp_path / "config" / "config.yaml"
        assert config_file.exists(), "config.yaml must be written when --server is passed"
        import yaml
        cfg = yaml.safe_load(config_file.read_text())
        assert cfg.get("server") == "https://agnes.example.com"

    def test_import_token_claim_fallback_via_cli_email_override(self, tmp_path, monkeypatch):
        """Missing email claim -> refuse without --email, accept with it.
        v19 dropped the --role flag (token.json no longer carries role)."""
        import jwt as pyjwt
        monkeypatch.setenv("AGNES_SERVER", "http://example.test")
        # JWT without email claim — simulates a malformed or minimal token.
        token = pyjwt.encode({"sub": "u-1", "typ": "pat"}, "unused", algorithm="HS256")

        with self._mock_verify(200):
            fail_result = runner.invoke(app, ["auth", "import-token", "--token", token])
        assert fail_result.exit_code == 1
        assert "missing" in fail_result.output.lower()

        with self._mock_verify(200):
            ok_result = runner.invoke(
                app,
                [
                    "auth", "import-token",
                    "--token", token,
                    "--email", "carol@example.com",
                ],
            )
        assert ok_result.exit_code == 0, ok_result.output
        token_file = tmp_path / "config" / "token.json"
        data = json.loads(token_file.read_text())
        assert data == {"access_token": token, "email": "carol@example.com"}


class TestAuthWhoami:
    def test_whoami_no_token(self):
        """Whoami exits when no token is stored."""
        with patch("cli.commands.auth.get_token", return_value=None):
            result = runner.invoke(app, ["auth", "whoami"])
        assert result.exit_code == 1
        assert "Not logged in" in result.output

    def test_whoami_valid_token(self):
        """Whoami decodes JWT and shows user info. v19: no role claim."""
        import jwt as pyjwt
        token = pyjwt.encode(
            {"email": "alice@example.com"},
            "secret",
            algorithm="HS256",
        )
        with patch("cli.commands.auth.get_token", return_value=token):
            with patch("cli.commands.auth.get_server_url", return_value="http://localhost:8000"):
                result = runner.invoke(app, ["auth", "whoami"])
        assert result.exit_code == 0
        assert "alice@example.com" in result.output

    def test_whoami_invalid_token(self):
        """Whoami with garbled token exits with error."""
        with patch("cli.commands.auth.get_token", return_value="not.a.jwt"):
            result = runner.invoke(app, ["auth", "whoami"])
        # May succeed or fail depending on jwt decode — either way no traceback
        assert result.exit_code in (0, 1)


def test_da_login_sends_password(monkeypatch):
    import httpx
    from typer.testing import CliRunner
    from cli.commands import auth as auth_mod

    captured = {}

    def fake_post(path, json=None, **kwargs):
        captured["path"] = path
        captured["json"] = json
        return httpx.Response(200, json={
            "access_token": "tok", "email": "u@t", "role": "analyst",
            "user_id": "u1", "token_type": "bearer",
        })

    monkeypatch.setattr(auth_mod, "api_post", fake_post, raising=False)

    runner = CliRunner()
    # Provide email and password via stdin (typer prompts)
    result = runner.invoke(auth_mod.auth_app, ["login"], input="u@t\nhunter2\n")
    assert result.exit_code == 0, result.output
    assert captured["path"] == "/auth/token"
    assert captured["json"] == {"email": "u@t", "password": "hunter2"}


def test_da_auth_token_create_calls_api(monkeypatch):
    import httpx
    from typer.testing import CliRunner
    from cli.commands.auth import auth_app
    from cli.commands import tokens as tok_mod

    captured = {}

    def fake_post(path, json=None, **kwargs):
        captured["path"] = path
        captured["json"] = json
        return httpx.Response(201, json={
            "id": "abc", "name": json["name"], "prefix": "XXXXXXXX",
            "token": "raw-token-once",
            "expires_at": None, "created_at": "2026-04-21T00:00:00+00:00",
        })

    monkeypatch.setattr(tok_mod, "api_post", fake_post, raising=False)

    runner = CliRunner()
    result = runner.invoke(auth_app, ["token", "create", "--name", "laptop", "--ttl", "30d"])
    assert result.exit_code == 0, result.output
    assert captured["path"] == "/auth/tokens"
    assert captured["json"] == {"name": "laptop", "expires_in_days": 30}
    assert "raw-token-once" in result.output
