import json
from datetime import UTC, datetime, timedelta
from pathlib import Path
from unittest.mock import patch

import pytest
from fastapi.testclient import TestClient
from pydantic import ValidationError
from pygitweb.auth import authenticate_http_key
from pygitweb.auth_config import AuthConfig, LocalUser
from pygitweb.config import Settings, settings
from pygitweb.conftest import set_client_session
from pygitweb.main import app
from pygitweb.password_hash import is_password_hash, verify_password
from pygitweb.sessions import create_session

_SALT = "0123456789abcdef"


def _config() -> AuthConfig:
	return AuthConfig(
		auth_mode="local",
		salt=_SALT,
		local_users=[LocalUser(user="admin", password="secret", email="admin@example.com")],
	)


def test_api_key_settings_default_to_two_keys_without_expiry() -> None:
	cfg = Settings(AUTH=False)
	assert cfg.API_KEY_MAX_COUNT == 2
	assert cfg.API_KEY_EXPIRY_DAYS == 0
	with pytest.raises(ValidationError):
		Settings(AUTH=False, API_KEY_MAX_COUNT=-1)


def test_issue_api_key_returns_plaintext_and_stores_hash(tmp_path: Path) -> None:
	cfg = _config()
	path = tmp_path / "auth.json"
	client = TestClient(app)
	with (
		patch.object(settings, "AUTH", True),
		patch.object(settings, "AUTH_CONFIG", str(path)),
		patch("pygitweb.auth_config.auth_config", cfg),
		patch("pygitweb.auth.auth_config", cfg),
		patch("pygitweb.main.auth_config", cfg),
	):
		set_client_session(client, "admin")
		page = client.get("/user")
		assert page.status_code == 200
		assert "API keys" in page.text
		assert "Create key" in page.text
		issued = client.post("/user/keys", json={"id": "laptop", "readonly": True})
		assert issued.status_code == 200, issued.text
		body = issued.json()
		assert body["id"] == "laptop"
		assert body["readonly"] is True
		plaintext = body["secret"]
		assert isinstance(plaintext, str)
		assert not plaintext.startswith("scrypt$")
		assert body["time_expiry"] is None
		stored = cfg.local_users[0].keys[0]
		assert stored.secret != plaintext
		assert is_password_hash(stored.secret)
		assert verify_password(plaintext, stored.secret, config_salt=_SALT)
		on_disk = json.loads(path.read_text(encoding="utf-8"))
		assert on_disk["local_users"][0]["keys"][0]["secret"] == stored.secret
		assert plaintext not in path.read_text(encoding="utf-8")
		matched = authenticate_http_key("admin", plaintext)
		assert matched is not None
		assert matched[1] is True
		again = client.get("/user")
		assert "laptop" in again.text
		assert plaintext not in again.text


def test_issue_api_key_enforces_limit_expiry_and_duplicate_id(tmp_path: Path) -> None:
	cfg = _config()
	path = tmp_path / "auth.json"
	client = TestClient(app)
	with (
		patch.object(settings, "AUTH", True),
		patch.object(settings, "AUTH_CONFIG", str(path)),
		patch.object(settings, "API_KEY_MAX_COUNT", 2),
		patch.object(settings, "API_KEY_EXPIRY_DAYS", 7),
		patch("pygitweb.auth_config.auth_config", cfg),
		patch("pygitweb.auth.auth_config", cfg),
		patch("pygitweb.main.auth_config", cfg),
	):
		set_client_session(client, "admin")
		before = datetime.now(UTC)
		first = client.post("/user/keys", json={"id": "alpha-key"})
		assert first.status_code == 200, first.text
		duplicate = client.post("/user/keys", json={"id": "alpha-key"})
		second = client.post("/user/keys", json={"id": "beta-key"})
		assert second.status_code == 200, second.text
		third = client.post("/user/keys", json={"id": "gamma-key"})
		assert duplicate.status_code == 409
		assert third.status_code == 409
		assert len(cfg.local_users[0].keys) == 2
		expiry = datetime.fromisoformat(first.json()["time_expiry"])
		assert before + timedelta(days=7) <= expiry <= datetime.now(UTC) + timedelta(days=7)
		assert second.json()["time_expiry"] is not None
		revoked = client.delete("/user/keys", params={"id": "alpha-key"})
		assert revoked.status_code == 204
		assert [key.id for key in cfg.local_users[0].keys] == ["beta-key"]
		page = client.get("/user")
		assert "alpha-key" not in page.text
		assert "beta-key" in page.text
		replaced = client.post("/user/keys", json={"id": "alpha-key"})
		assert replaced.status_code == 200
		assert replaced.json()["id"] == "alpha-key"


def test_issue_api_key_requires_a_local_account_and_a_session(tmp_path: Path) -> None:
	cfg = _config()
	path = tmp_path / "auth.json"
	client = TestClient(app)
	with (
		patch.object(settings, "AUTH", True),
		patch.object(settings, "AUTH_CONFIG", str(path)),
		patch("pygitweb.auth_config.auth_config", cfg),
		patch("pygitweb.auth.auth_config", cfg),
		patch("pygitweb.main.auth_config", cfg),
	):
		missing = client.post("/user/keys", json={"id": "laptop"})
		assert missing.status_code == 401
		token = create_session("nobody", auth_method="local")
		unknown = client.post(
			"/user/keys",
			json={"id": "laptop"},
			headers={"Authorization": f"Bearer {token}"},
		)
		assert unknown.status_code == 404
		page = client.get("/user", headers={"Authorization": f"Bearer {token}"})
		assert "cannot be stored" in page.text
		email_token = create_session("other-name", email="Admin@Example.com", auth_method="oauth")
		by_email = client.post(
			"/user/keys",
			json={"id": "mail-key"},
			headers={"Authorization": f"Bearer {email_token}"},
		)
		assert by_email.status_code == 200, by_email.text
		assert cfg.local_users[0].keys[0].id == "mail-key"
		missing_key = client.delete(
			"/user/keys", params={"id": "missing"}, headers={"Authorization": f"Bearer {email_token}"}
		)
		assert missing_key.status_code == 404
		revoke_unknown = client.delete(
			"/user/keys", params={"id": "mail-key"}, headers={"Authorization": f"Bearer {token}"}
		)
		assert revoke_unknown.status_code == 404
	with patch.object(settings, "AUTH", False):
		disabled = client.post("/user/keys", json={"id": "laptop"})
		assert disabled.status_code == 400
		disabled_revoke = client.delete("/user/keys", params={"id": "laptop"})
		assert disabled_revoke.status_code == 400


def test_issue_api_key_keeps_memory_unchanged_when_save_fails(tmp_path: Path) -> None:
	cfg = _config()
	path = tmp_path / "auth.json"
	client = TestClient(app, raise_server_exceptions=False)
	with (
		patch.object(settings, "AUTH", True),
		patch.object(settings, "AUTH_CONFIG", str(path)),
		patch("pygitweb.auth_config.auth_config", cfg),
		patch("pygitweb.auth.auth_config", cfg),
		patch("pygitweb.main.auth_config", cfg),
		patch("pygitweb.auth.write_auth_config", side_effect=OSError("disk")),
	):
		set_client_session(client, "admin")
		response = client.post("/user/keys", json={"id": "laptop"})
		assert response.status_code == 500
		assert cfg.local_users[0].keys == []


def test_revoke_api_key_keeps_memory_unchanged_when_save_fails(tmp_path: Path) -> None:
	cfg = _config()
	path = tmp_path / "auth.json"
	client = TestClient(app, raise_server_exceptions=False)
	with (
		patch.object(settings, "AUTH", True),
		patch.object(settings, "AUTH_CONFIG", str(path)),
		patch("pygitweb.auth_config.auth_config", cfg),
		patch("pygitweb.auth.auth_config", cfg),
		patch("pygitweb.main.auth_config", cfg),
	):
		set_client_session(client, "admin")
		issued = client.post("/user/keys", json={"id": "laptop"})
		assert issued.status_code == 200, issued.text
		with patch("pygitweb.auth.write_auth_config", side_effect=OSError("disk")):
			response = client.delete("/user/keys", params={"id": "laptop"})
		assert response.status_code == 500
		assert [key.id for key in cfg.local_users[0].keys] == ["laptop"]