"""Tests for project_hub/forge/auth.py — unified forge auth resolution."""

from __future__ import annotations

import json
import os
import stat
from pathlib import Path

import pytest

import project_hub.forge.auth as forge_auth


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def tmp_auth_file(tmp_path, monkeypatch):
    """Return a tmp auth.json path and point AUTH_FILE at it."""
    auth_file = tmp_path / "auth.json"
    monkeypatch.setattr(forge_auth, "AUTH_FILE", auth_file)
    return auth_file


@pytest.fixture()
def tmp_python_gitlab_cfg(tmp_path, monkeypatch):
    """Return a tmp .python-gitlab.cfg path and point PYTHON_GITLAB_CFG at it."""
    cfg_file = tmp_path / "python-gitlab.cfg"
    monkeypatch.setattr(forge_auth, "PYTHON_GITLAB_CFG", cfg_file)
    return cfg_file


@pytest.fixture(autouse=True)
def clean_env(monkeypatch):
    """Remove forge env vars from the test environment by default."""
    monkeypatch.delenv("GITLAB_TOKEN", raising=False)
    monkeypatch.delenv("GITHUB_TOKEN", raising=False)


## no_keyring is handled by conftest._no_real_keyring (autouse)


# ---------------------------------------------------------------------------
# resolve_token — auth.json
# ---------------------------------------------------------------------------


def test_resolve_from_auth_file(tmp_auth_file):
    """Token stored in auth.json is returned for any forge_type."""
    tmp_auth_file.write_text(json.dumps({"gitlab.example.com": "secret123"}))
    assert forge_auth.resolve_token("gitlab.example.com") == "secret123"
    assert forge_auth.resolve_token("gitlab.example.com", forge_type="gitlab") == "secret123"
    assert forge_auth.resolve_token("gitlab.example.com", forge_type="github") == "secret123"


def test_resolve_auth_file_missing_returns_none(tmp_auth_file):
    """No auth.json → None."""
    assert forge_auth.resolve_token("gitlab.com") is None


def test_resolve_auth_file_unknown_instance_returns_none(tmp_auth_file):
    """Instance not in auth.json → None (no forge env fallbacks active)."""
    tmp_auth_file.write_text(json.dumps({"other.example.com": "tok"}))
    assert forge_auth.resolve_token("gitlab.com", forge_type=None) is None


# ---------------------------------------------------------------------------
# resolve_token — GITLAB_TOKEN env fallback
# ---------------------------------------------------------------------------


def test_gitlab_token_env_gitlab_com(tmp_auth_file, monkeypatch):
    """GITLAB_TOKEN activates for forge_type='gitlab' on gitlab.com."""
    monkeypatch.setenv("GITLAB_TOKEN", "gl-env-token")
    assert forge_auth.resolve_token("gitlab.com", forge_type="gitlab") == "gl-env-token"


def test_gitlab_token_env_not_for_self_hosted(tmp_auth_file, monkeypatch):
    """GITLAB_TOKEN does NOT activate for self-hosted instances."""
    monkeypatch.setenv("GITLAB_TOKEN", "gl-env-token")
    assert forge_auth.resolve_token("gitlab.example.com", forge_type="gitlab") is None


def test_gitlab_token_env_not_for_github_type(tmp_auth_file, monkeypatch):
    """GITLAB_TOKEN does NOT activate when forge_type='github'."""
    monkeypatch.setenv("GITLAB_TOKEN", "gl-env-token")
    assert forge_auth.resolve_token("gitlab.com", forge_type="github") is None


def test_gitlab_token_env_not_for_none_type(tmp_auth_file, monkeypatch):
    """GITLAB_TOKEN does NOT activate when forge_type=None."""
    monkeypatch.setenv("GITLAB_TOKEN", "gl-env-token")
    assert forge_auth.resolve_token("gitlab.com", forge_type=None) is None


# ---------------------------------------------------------------------------
# resolve_token — GITHUB_TOKEN env fallback
# ---------------------------------------------------------------------------


def test_github_token_env_github_type(tmp_auth_file, monkeypatch):
    """GITHUB_TOKEN activates for forge_type='github'."""
    monkeypatch.setenv("GITHUB_TOKEN", "gh-env-token")
    assert forge_auth.resolve_token("github.com", forge_type="github") == "gh-env-token"


def test_github_token_env_not_for_gitlab_type(tmp_auth_file, monkeypatch):
    """GITHUB_TOKEN does NOT activate when forge_type='gitlab'."""
    monkeypatch.setenv("GITHUB_TOKEN", "gh-env-token")
    assert forge_auth.resolve_token("github.com", forge_type="gitlab") is None


def test_github_token_env_not_for_none_type(tmp_auth_file, monkeypatch):
    """GITHUB_TOKEN does NOT activate when forge_type=None."""
    monkeypatch.setenv("GITHUB_TOKEN", "gh-env-token")
    assert forge_auth.resolve_token("github.com", forge_type=None) is None


# ---------------------------------------------------------------------------
# resolve_token — python-gitlab.cfg fallback
# ---------------------------------------------------------------------------


def test_python_gitlab_cfg_fallback(tmp_auth_file, tmp_python_gitlab_cfg, monkeypatch):
    """~/.python-gitlab.cfg is used for forge_type='gitlab'."""
    tmp_python_gitlab_cfg.write_text(
        "[global]\n"
        "default = mylab\n"
        "[mylab]\n"
        "url = https://gitlab.example.com\n"
        "private_token = cfg-token\n"
    )
    result = forge_auth.resolve_token("gitlab.example.com", forge_type="gitlab")
    assert result == "cfg-token"


def test_python_gitlab_cfg_not_for_github_type(tmp_auth_file, tmp_python_gitlab_cfg, monkeypatch):
    """python-gitlab.cfg is NOT consulted for forge_type='github'."""
    tmp_python_gitlab_cfg.write_text(
        "[global]\n"
        "[mylab]\n"
        "url = https://gitlab.example.com\n"
        "private_token = cfg-token\n"
    )
    result = forge_auth.resolve_token("gitlab.example.com", forge_type="github")
    assert result is None


def test_python_gitlab_cfg_not_for_none_type(tmp_auth_file, tmp_python_gitlab_cfg):
    """python-gitlab.cfg is NOT consulted when forge_type=None."""
    tmp_python_gitlab_cfg.write_text(
        "[global]\n"
        "[mylab]\n"
        "url = https://gitlab.example.com\n"
        "private_token = cfg-token\n"
    )
    result = forge_auth.resolve_token("gitlab.example.com", forge_type=None)
    assert result is None


# ---------------------------------------------------------------------------
# auth.json takes precedence over env vars
# ---------------------------------------------------------------------------


def test_auth_file_takes_precedence_over_env(tmp_auth_file, monkeypatch):
    """auth.json token wins over GITLAB_TOKEN env."""
    tmp_auth_file.write_text(json.dumps({"gitlab.com": "file-token"}))
    monkeypatch.setenv("GITLAB_TOKEN", "env-token")
    assert forge_auth.resolve_token("gitlab.com", forge_type="gitlab") == "file-token"


# ---------------------------------------------------------------------------
# store_token / round-trip
# ---------------------------------------------------------------------------


def test_store_token_round_trip(tmp_auth_file):
    """store_token persists and resolve_token retrieves the token."""
    forge_auth.store_token("myforge.example.com", "round-trip-token")
    assert forge_auth.resolve_token("myforge.example.com") == "round-trip-token"


def test_store_token_sets_permissions(tmp_auth_file):
    """store_token creates auth.json with 0o600 permissions."""
    forge_auth.store_token("myforge.example.com", "tok")
    mode = tmp_auth_file.stat().st_mode & 0o777
    assert mode == 0o600


def test_store_token_preserves_existing(tmp_auth_file):
    """Storing a second instance doesn't erase the first."""
    forge_auth.store_token("forge-a.example.com", "token-a")
    forge_auth.store_token("forge-b.example.com", "token-b")
    assert forge_auth.resolve_token("forge-a.example.com") == "token-a"
    assert forge_auth.resolve_token("forge-b.example.com") == "token-b"


# ---------------------------------------------------------------------------
# auth_status
# ---------------------------------------------------------------------------


def test_auth_status_returns_all_instances(tmp_auth_file):
    """auth_status returns one AuthInfo per instance in auth.json."""
    tmp_auth_file.write_text(json.dumps({
        "forge-a.example.com": "tok-a",
        "forge-b.example.com": "tok-b",
    }))
    statuses = forge_auth.auth_status()
    assert len(statuses) == 2
    instances = {s.instance for s in statuses}
    assert instances == {"forge-a.example.com", "forge-b.example.com"}
    for s in statuses:
        assert s.has_token is True
        assert s.authenticated is True


def test_auth_status_empty_when_no_file(tmp_auth_file):
    """auth_status returns [] when auth.json doesn't exist."""
    assert forge_auth.auth_status() == []


# ---------------------------------------------------------------------------
# Permissions auto-fix
# ---------------------------------------------------------------------------


def test_permissions_auto_restricted(tmp_auth_file):
    """auth.json with too-open permissions is automatically restricted to 0600."""
    tmp_auth_file.write_text(json.dumps({"forge.example.com": "tok"}))
    tmp_auth_file.chmod(0o644)
    assert forge_auth.resolve_token("forge.example.com") == "tok"
    actual_mode = tmp_auth_file.stat().st_mode & 0o777
    assert actual_mode == 0o600


# ---------------------------------------------------------------------------
# Keyring integration
# ---------------------------------------------------------------------------


def _fake_keyring_store():
    """Return (get, set, available) fakes backed by a dict."""
    store = {}

    def get(instance):
        return store.get(instance)

    def put(instance, token):
        store[instance] = token
        return True

    def available():
        return True

    return get, put, available, store


def test_store_token_prefers_keyring(tmp_auth_file, monkeypatch):
    """When keyring is available, store_token writes sentinel to auth.json."""
    get, put, available, store = _fake_keyring_store()
    monkeypatch.setattr(forge_auth, "_resolve_from_keyring", get)
    monkeypatch.setattr(forge_auth, "_store_to_keyring", put)
    monkeypatch.setattr(forge_auth, "_keyring_available", available)

    forge_auth.store_token("forge.example.com", "my-secret-pat")

    # auth.json has sentinel, not the real token
    data = json.loads(tmp_auth_file.read_text())
    assert data["forge.example.com"] == forge_auth._KEYRING_SENTINEL

    # token is in the fake keyring
    assert store["forge.example.com"] == "my-secret-pat"


def test_resolve_token_keyring_first(tmp_auth_file, monkeypatch):
    """Keyring takes precedence over auth.json."""
    tmp_auth_file.write_text(json.dumps({"forge.example.com": "file-token"}))
    monkeypatch.setattr(forge_auth, "_resolve_from_keyring", lambda i: "keyring-token" if i == "forge.example.com" else None)

    assert forge_auth.resolve_token("forge.example.com") == "keyring-token"


def test_resolve_token_falls_back_to_file(tmp_auth_file, monkeypatch):
    """When keyring returns None, falls back to auth.json."""
    tmp_auth_file.write_text(json.dumps({"forge.example.com": "file-token"}))
    # no_keyring autouse fixture already sets _resolve_from_keyring to return None
    assert forge_auth.resolve_token("forge.example.com") == "file-token"


def test_resolve_token_skips_sentinel(tmp_auth_file):
    """A <keyring> sentinel in auth.json does not get returned as a token."""
    tmp_auth_file.write_text(json.dumps({"forge.example.com": forge_auth._KEYRING_SENTINEL}))
    # keyring disabled by autouse fixture → sentinel skipped → None
    assert forge_auth.resolve_token("forge.example.com") is None


def test_auth_status_keyring_backend(tmp_auth_file, monkeypatch):
    """auth_status reports backend='keyring' for sentinel entries."""
    tmp_auth_file.write_text(json.dumps({
        "kr.example.com": forge_auth._KEYRING_SENTINEL,
        "file.example.com": "plain-token",
    }))
    monkeypatch.setattr(forge_auth, "_resolve_from_keyring", lambda i: "tok" if i == "kr.example.com" else None)

    statuses = forge_auth.auth_status()
    by_inst = {s.instance: s for s in statuses}
    assert by_inst["kr.example.com"].backend == "keyring"
    assert by_inst["kr.example.com"].has_token is True
    assert by_inst["file.example.com"].backend == "file"


def test_migration_warning_when_file_backed(tmp_auth_file, monkeypatch):
    """migration_warning returns a message when file-backed tokens exist and keyring is available."""
    tmp_auth_file.write_text(json.dumps({"forge.example.com": "plaintext-tok"}))
    monkeypatch.setattr(forge_auth, "_keyring_available", lambda: True)

    warning = forge_auth.migration_warning()
    assert warning is not None
    assert "forge.example.com" in warning
    assert "project-hub auth migrate" in warning


def test_migration_warning_none_when_all_keyring(tmp_auth_file, monkeypatch):
    """migration_warning returns None when all tokens are in keyring."""
    tmp_auth_file.write_text(json.dumps({"forge.example.com": forge_auth._KEYRING_SENTINEL}))
    monkeypatch.setattr(forge_auth, "_keyring_available", lambda: True)

    assert forge_auth.migration_warning() is None


def test_migration_warning_none_when_keyring_unavailable(tmp_auth_file, monkeypatch):
    """migration_warning returns None when keyring backend is not available."""
    tmp_auth_file.write_text(json.dumps({"forge.example.com": "plaintext-tok"}))
    monkeypatch.setattr(forge_auth, "_keyring_available", lambda: False)

    assert forge_auth.migration_warning() is None


def test_migrate_to_keyring(tmp_auth_file, monkeypatch):
    """migrate_to_keyring moves file tokens to keyring and writes sentinel."""
    tmp_auth_file.write_text(json.dumps({
        "a.example.com": "token-a",
        "b.example.com": forge_auth._KEYRING_SENTINEL,
    }))
    get, put, available, store = _fake_keyring_store()
    monkeypatch.setattr(forge_auth, "_store_to_keyring", put)

    migrated, failed = forge_auth.migrate_to_keyring()

    assert migrated == ["a.example.com"]
    assert failed == []
    assert store["a.example.com"] == "token-a"

    # auth.json now has sentinel for migrated entry
    data = json.loads(tmp_auth_file.read_text())
    assert data["a.example.com"] == forge_auth._KEYRING_SENTINEL
    assert data["b.example.com"] == forge_auth._KEYRING_SENTINEL


def test_migrate_to_keyring_partial_failure(tmp_auth_file, monkeypatch):
    """migrate_to_keyring reports failed instances."""
    tmp_auth_file.write_text(json.dumps({
        "ok.example.com": "token-ok",
        "fail.example.com": "token-fail",
    }))

    def selective_store(instance, token):
        if instance == "fail.example.com":
            return False
        return True

    monkeypatch.setattr(forge_auth, "_store_to_keyring", selective_store)

    migrated, failed = forge_auth.migrate_to_keyring()
    assert migrated == ["ok.example.com"]
    assert failed == ["fail.example.com"]

    # Only the successful one is sentinel'd
    data = json.loads(tmp_auth_file.read_text())
    assert data["ok.example.com"] == forge_auth._KEYRING_SENTINEL
    assert data["fail.example.com"] == "token-fail"


# ---------------------------------------------------------------------------
# forge/__init__.py re-exports
# ---------------------------------------------------------------------------


def test_forge_package_re_exports():
    """Key symbols are importable from project_hub.forge."""
    from project_hub.forge import (  # noqa: F401
        AuthInfo, auth_status, migrate_to_keyring, migration_warning,
        resolve_token, store_token,
    )
    assert callable(resolve_token)
    assert callable(store_token)
    assert callable(auth_status)
    assert callable(migrate_to_keyring)
    assert callable(migration_warning)
    assert isinstance(AuthInfo("x", True, True), AuthInfo)
