# -*- coding: utf-8 -*-
"""
test_service_manage.py — 服务管理模块业务层单元测试

被测模块：skill/code/web/services/service_manage.py

覆盖：
  1. 目标配置 CRUD（list / create / update / delete / get_view）
  2. 校验规则（名称/主机/协议/认证方式/端口/密码）
  3. 密码脱敏（has_password 布尔，不返回密文）
  4. test_connection 连通性测试（mock urllib）
  5. 项目信息保存/读取（密码脱敏）
  6. 文件占位生成与上传保存

所有测试用 tmp_path 隔离数据目录，不触碰真实 service_manage_data。
"""

# conftest 已把 skill/code/web 加入 sys.path
import json
from pathlib import Path

import pytest

import services.service_manage as sm
from utils import paths


@pytest.fixture
def manage(tmp_path, monkeypatch):
    """把 service_manage 模块级数据路径指向 tmp_path 隔离目录。"""
    data_dir = tmp_path / "service_manage_data"
    monkeypatch.setattr(sm, "SERVICE_MANAGE_DATA_DIR", data_dir)
    monkeypatch.setattr(sm, "MANAGE_TARGETS_FILE", data_dir / "targets.json")
    monkeypatch.setattr(sm, "MANAGE_PROJECTS_FILE", data_dir / "projects.json")
    monkeypatch.setattr(sm, "MANAGE_UPLOADS_DIR", data_dir / "uploads")
    return data_dir


# ============================================================
# 目标配置 CRUD
# ============================================================

class TestTargetCrud:
    def test_create_and_list(self, manage):
        """新增目标后列表能读到，且密码已加密脱敏。"""
        t = sm.create_target({
            "name": "5.44平台",
            "host": "192.168.5.44",
            "port": 443,
            "protocol": "https",
            "auth_type": "password",
            "username": "admin",
            "password": "secret123",
        })
        assert t["id"]
        assert t["name"] == "5.44平台"
        assert t["host"] == "192.168.5.44"
        assert t["has_password"] is True
        # 对外视图不含密码密文
        assert "password" not in t
        assert "password_enc" not in t

        targets = sm.list_targets()
        assert len(targets) == 1
        assert targets[0]["has_password"] is True
        # 存储的是密文而非明文
        stored = json.loads((manage / "targets.json").read_text(encoding="utf-8"))
        assert stored[0]["password_enc"] != "secret123"

    def test_create_requires_name(self, manage):
        """名称为空时报错。"""
        with pytest.raises(ValueError):
            sm.create_target({"name": "", "host": "1.2.3.4"})

    def test_create_rejects_bad_host(self, manage):
        """非法主机地址报错。"""
        with pytest.raises(ValueError):
            sm.create_target({"name": "x", "host": "http://bad host!"})

    def test_create_rejects_bad_protocol(self, manage):
        """协议非法报错。"""
        with pytest.raises(ValueError):
            sm.create_target({"name": "x", "host": "1.2.3.4", "protocol": "ftp"})

    def test_create_requires_password_for_password_auth(self, manage):
        """密码认证方式必须提供用户名和密码。"""
        with pytest.raises(ValueError):
            sm.create_target({"name": "x", "host": "1.2.3.4", "auth_type": "password"})
        with pytest.raises(ValueError):
            sm.create_target({
                "name": "x", "host": "1.2.3.4", "auth_type": "password",
                "username": "u",
            })

    def test_create_rejects_bad_port(self, manage):
        """端口越界报错。"""
        with pytest.raises(ValueError):
            sm.create_target({"name": "x", "host": "1.2.3.4", "port": 70000})

    def test_create_token_auth_no_password(self, manage):
        """Token 认证方式无需密码，username 可为空。"""
        t = sm.create_target({
            "name": "x", "host": "1.2.3.4", "auth_type": "token",
        })
        assert t["username"] == ""
        assert t["has_password"] is False

    def test_update_target(self, manage):
        """更新名称、主机、密码。"""
        t = sm.create_target({
            "name": "旧名", "host": "1.1.1.1", "port": 443,
            "auth_type": "token",
        })
        updated = sm.update_target(t["id"], {
            "name": "新名", "host": "2.2.2.2", "password": "newpass",
        })
        assert updated["name"] == "新名"
        assert updated["host"] == "2.2.2.2"
        assert updated["has_password"] is True

    def test_update_missing_target(self, manage):
        """更新不存在目标报错。"""
        with pytest.raises(ValueError):
            sm.update_target("nope", {"name": "x"})

    def test_delete_target(self, manage):
        """删除目标。"""
        t = sm.create_target({"name": "x", "host": "1.2.3.4", "auth_type": "token"})
        assert sm.delete_target(t["id"]) is True
        assert sm.delete_target(t["id"]) is False  # 已删除
        assert sm.list_targets() == []

    def test_get_target_view(self, manage):
        """按 id 获取脱敏视图。"""
        t = sm.create_target({"name": "x", "host": "1.2.3.4", "auth_type": "token"})
        view = sm.get_target_view(t["id"])
        assert view["host"] == "1.2.3.4"
        assert "password_enc" not in view
        assert sm.get_target_view("nope") is None


# ============================================================
# 连通性测试
# ============================================================

class TestConnection:
    def _fake_urlopen_success(self, req, timeout=10):
        class _Resp:
            def __init__(self):
                self._code = 200
            def getcode(self):
                return self._code
            def __enter__(self):
                return self
            def __exit__(self, *a):
                return False
        return _Resp()

    def test_connection_success(self, manage, monkeypatch):
        """HTTP 200 返回成功。"""
        monkeypatch.setattr("urllib.request.urlopen", self._fake_urlopen_success)
        res = sm.test_connection({"host": "192.168.5.44", "port": 443, "protocol": "https"})
        assert res["success"] is True

    def test_connection_bad_host(self, manage):
        """非法主机返回失败而非抛异常。"""
        res = sm.test_connection({"host": "bad host!"})
        assert res["success"] is False

    def test_connection_exception(self, manage, monkeypatch):
        """连接异常返回失败信息。"""
        def _boom(req, timeout=10):
            raise ConnectionError("timeout")
        monkeypatch.setattr("urllib.request.urlopen", _boom)
        res = sm.test_connection({"host": "10.0.0.1", "port": 9999})
        assert res["success"] is False
        assert "timeout" in res["message"]


# ============================================================
# 项目信息
# ============================================================

class TestProject:
    def test_save_and_get(self, manage):
        """保存项目信息后能读取，密码脱敏。"""
        res = sm.save_project({
            "salesPeople": "张三",
            "deployer": "李四",
            "orderTime": "2026-08-01 10:00:00",
            "serverPassword": "pwd123",
            "projectOverview": "某项目",
        })
        assert res["success"] is True

        p = sm.get_project()
        assert p["salesPeople"] == "张三"
        assert p["deployer"] == "李四"
        assert p["has_password"] is True
        assert "serverPassword" not in p

    def test_get_no_project(self, manage):
        """无项目信息返回 None。"""
        assert sm.get_project() is None


# ============================================================
# 文件操作
# ============================================================

class TestFiles:
    def test_activation_file(self, manage):
        """激活文件生成在 uploads 目录。"""
        p = sm.get_activation_file()
        assert p.exists()
        assert p.name == "licences.zip"

    def test_deploy_view_file(self, manage):
        """部署视图文件带时间戳。"""
        p = sm.get_deploy_view_file()
        assert p.exists()
        assert p.name.startswith("deploymentView")
        assert p.name.endswith(".xlsx")

    def test_self_check_file(self, manage):
        """自检文档文件带时间戳。"""
        p = sm.get_self_check_file()
        assert p.exists()
        assert p.name.startswith("checkList")
        assert p.name.endswith(".xlsx")

    def test_save_uploaded_file(self, manage):
        """上传文件保存到子目录。"""
        class _File:
            def __init__(self, name, content):
                self.filename = name
                self._content = content
            def save(self, path):
                Path(path).write_bytes(self._content)

        f = _File("auth.zip", b"binary-data")
        res = sm.save_uploaded_file(f, subdir="license")
        assert res["success"] is True
        assert res["filename"] == "auth.zip"
        assert res["size"] == len(b"binary-data")
        saved = manage / "uploads" / "license" / "auth.zip"
        assert saved.exists()