# -*- coding: utf-8 -*-
"""环境配置路由单元测试

覆盖 3 个端点：GET / PUT / GET /export
通过 mock 绕过文件 I/O，专注路由逻辑验证。
"""

import os

os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-unit-tests")

from unittest.mock import patch, MagicMock

import pytest
from fastapi.testclient import TestClient

from app.auth.dependencies import CurrentUser, get_current_user
from app.main import app

_TEST_USER = CurrentUser(user_id=1, site_id=100)


def _override_auth():
    return _TEST_USER


app.dependency_overrides[get_current_user] = _override_auth
client = TestClient(app)

# 模拟 .env 文件内容
_SAMPLE_ENV = """\
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_PASSWORD=super_secret_123
JWT_SECRET_KEY=my-jwt-secret

# ETL 配置
ETL_DB_DSN=postgresql://user:pass@host/db
TIMEZONE=Asia/Shanghai
"""

_MOCK_ENV_PATH = "app.routers.env_config._ENV_PATH"


def _mock_path(content: str | None = _SAMPLE_ENV, exists: bool = True):
    """构造 mock Path 对象。"""
    mock = MagicMock()
    mock.exists.return_value = exists
    if content is not None:
        mock.read_text.return_value = content
    return mock


# ---------------------------------------------------------------------------
# GET /api/env-config
# ---------------------------------------------------------------------------

class TestGetEnvConfig:

    @patch(_MOCK_ENV_PATH)
    def test_returns_entries_with_masked_sensitive(self, mock_path_obj):
        mock_path_obj.__class__ = type(MagicMock())
        mock_path_obj.exists.return_value = True
        mock_path_obj.read_text.return_value = _SAMPLE_ENV

        resp = client.get("/api/env-config")
        assert resp.status_code == 200
        data = resp.json()
        entries = {e["key"]: e["value"] for e in data["entries"]}

        # 非敏感值原样返回
        assert entries["DB_HOST"] == "localhost"
        assert entries["DB_PORT"] == "5432"
        assert entries["TIMEZONE"] == "Asia/Shanghai"

        # 敏感值掩码
        assert entries["DB_PASSWORD"] == "****"
        assert entries["JWT_SECRET_KEY"] == "****"
        assert entries["ETL_DB_DSN"] == "****"

    @patch(_MOCK_ENV_PATH)
    def test_file_not_found(self, mock_path_obj):
        mock_path_obj.exists.return_value = False

        resp = client.get("/api/env-config")
        assert resp.status_code == 404

    @patch(_MOCK_ENV_PATH)
    def test_empty_file(self, mock_path_obj):
        mock_path_obj.exists.return_value = True
        mock_path_obj.read_text.return_value = ""

        resp = client.get("/api/env-config")
        assert resp.status_code == 200
        assert resp.json()["entries"] == []

    @patch(_MOCK_ENV_PATH)
    def test_comments_and_blank_lines_excluded(self, mock_path_obj):
        mock_path_obj.exists.return_value = True
        mock_path_obj.read_text.return_value = "# comment\n\nKEY=val\n"

        resp = client.get("/api/env-config")
        assert resp.status_code == 200
        entries = resp.json()["entries"]
        assert len(entries) == 1
        assert entries[0]["key"] == "KEY"


# ---------------------------------------------------------------------------
# PUT /api/env-config
# ---------------------------------------------------------------------------

class TestUpdateEnvConfig:

    @patch(_MOCK_ENV_PATH)
    def test_update_existing_key(self, mock_path_obj):
        mock_path_obj.exists.return_value = True
        mock_path_obj.read_text.return_value = "DB_HOST=localhost\nDB_PORT=5432\n"

        resp = client.put("/api/env-config", json={
            "entries": [
                {"key": "DB_HOST", "value": "192.168.1.1"},
                {"key": "DB_PORT", "value": "5433"},
            ]
        })
        assert resp.status_code == 200

        # 验证写入内容
        written = mock_path_obj.write_text.call_args[0][0]
        assert "DB_HOST=192.168.1.1" in written
        assert "DB_PORT=5433" in written

    @patch(_MOCK_ENV_PATH)
    def test_add_new_key(self, mock_path_obj):
        mock_path_obj.exists.return_value = True
        mock_path_obj.read_text.return_value = "DB_HOST=localhost\n"

        resp = client.put("/api/env-config", json={
            "entries": [
                {"key": "DB_HOST", "value": "localhost"},
                {"key": "NEW_KEY", "value": "new_value"},
            ]
        })
        assert resp.status_code == 200

        written = mock_path_obj.write_text.call_args[0][0]
        assert "NEW_KEY=new_value" in written

    @patch(_MOCK_ENV_PATH)
    def test_masked_value_preserves_original(self, mock_path_obj):
        """掩码值（****）不应覆盖原始敏感值。"""
        mock_path_obj.exists.return_value = True
        mock_path_obj.read_text.return_value = "DB_PASSWORD=real_secret\nDB_HOST=localhost\n"

        resp = client.put("/api/env-config", json={
            "entries": [
                {"key": "DB_PASSWORD", "value": "****"},
                {"key": "DB_HOST", "value": "newhost"},
            ]
        })
        assert resp.status_code == 200

        written = mock_path_obj.write_text.call_args[0][0]
        # 原始密码应保留
        assert "DB_PASSWORD=real_secret" in written
        assert "DB_HOST=newhost" in written

    @patch(_MOCK_ENV_PATH)
    def test_preserves_comments(self, mock_path_obj):
        mock_path_obj.exists.return_value = True
        mock_path_obj.read_text.return_value = "# 注释行\nDB_HOST=localhost\n\n# 另一个注释\n"

        resp = client.put("/api/env-config", json={
            "entries": [{"key": "DB_HOST", "value": "newhost"}]
        })
        assert resp.status_code == 200

        written = mock_path_obj.write_text.call_args[0][0]
        assert "# 注释行" in written
        assert "# 另一个注释" in written

    def test_invalid_key_format(self):
        resp = client.put("/api/env-config", json={
            "entries": [{"key": "123BAD", "value": "val"}]
        })
        assert resp.status_code == 422

    def test_empty_key(self):
        resp = client.put("/api/env-config", json={
            "entries": [{"key": "", "value": "val"}]
        })
        assert resp.status_code == 422

    @patch(_MOCK_ENV_PATH)
    def test_file_not_exists_creates_new(self, mock_path_obj):
        """文件不存在时，应创建新文件。"""
        mock_path_obj.exists.return_value = False

        resp = client.put("/api/env-config", json={
            "entries": [{"key": "NEW_KEY", "value": "value"}]
        })
        assert resp.status_code == 200

        written = mock_path_obj.write_text.call_args[0][0]
        assert "NEW_KEY=value" in written

    @patch(_MOCK_ENV_PATH)
    def test_update_sensitive_with_new_value(self, mock_path_obj):
        """显式提供新密码时应更新。"""
        mock_path_obj.exists.return_value = True
        mock_path_obj.read_text.return_value = "DB_PASSWORD=old_secret\n"

        resp = client.put("/api/env-config", json={
            "entries": [{"key": "DB_PASSWORD", "value": "new_secret"}]
        })
        assert resp.status_code == 200

        written = mock_path_obj.write_text.call_args[0][0]
        assert "DB_PASSWORD=new_secret" in written

        # 返回值中敏感键仍然掩码
        entries = {e["key"]: e["value"] for e in resp.json()["entries"]}
        assert entries["DB_PASSWORD"] == "****"


# ---------------------------------------------------------------------------
# GET /api/env-config/export
# ---------------------------------------------------------------------------

class TestExportEnvConfig:

    @patch(_MOCK_ENV_PATH)
    def test_export_masks_sensitive(self, mock_path_obj):
        mock_path_obj.exists.return_value = True
        mock_path_obj.read_text.return_value = _SAMPLE_ENV

        resp = client.get("/api/env-config/export")
        assert resp.status_code == 200
        assert resp.headers["content-type"].startswith("text/plain")
        assert "attachment" in resp.headers.get("content-disposition", "")

        content = resp.text
        # 非敏感值保留
        assert "DB_HOST=localhost" in content
        assert "TIMEZONE=Asia/Shanghai" in content

        # 敏感值掩码
        assert "super_secret_123" not in content
        assert "my-jwt-secret" not in content
        assert "DB_PASSWORD=****" in content
        assert "JWT_SECRET_KEY=****" in content

    @patch(_MOCK_ENV_PATH)
    def test_export_preserves_comments(self, mock_path_obj):
        mock_path_obj.exists.return_value = True
        mock_path_obj.read_text.return_value = _SAMPLE_ENV

        content = client.get("/api/env-config/export").text
        assert "# 数据库配置" in content
        assert "# ETL 配置" in content

    @patch(_MOCK_ENV_PATH)
    def test_export_file_not_found(self, mock_path_obj):
        mock_path_obj.exists.return_value = False

        resp = client.get("/api/env-config/export")
        assert resp.status_code == 404


# ---------------------------------------------------------------------------
# 认证测试
# ---------------------------------------------------------------------------

class TestEnvConfigAuth:

    def test_requires_auth(self):
        """移除 auth override 后，所有端点应返回 401/403。"""
        # 临时移除 override
        original = app.dependency_overrides.pop(get_current_user, None)
        try:
            for method, url in [
                ("GET", "/api/env-config"),
                ("PUT", "/api/env-config"),
                ("GET", "/api/env-config/export"),
            ]:
                resp = client.request(method, url)
                assert resp.status_code in (401, 403), f"{method} {url} 应需要认证"
        finally:
            if original:
                app.dependency_overrides[get_current_user] = original
