#!/usr/bin/env python3
"""
WebUI Tests - 测试Ascend Deployer WebUI的语法和功能
包括:JavaScript语法检查、Python语法检查、配置文件验证、Flask路由测试
"""

import os
import json
import yaml
import subprocess
import importlib.util
import shutil
from pathlib import Path
from typing import Dict, List

import pytest

PROJECT_ROOT = Path(__file__).parent.parent.parent
WEBUI_DIR = PROJECT_ROOT / "kadt" / "webui"

KEY_PROFILES = "profiles"
KEY_WORKFLOW = "workflow"
KEY_NAME = "name"
KEY_PARAMS = "params"
KEY_TYPE = "type"
KEY_TEMPLATES = "templates"
KEY_NODE = "node"
KEY_FALSE = "false"
KEY_TRUE = "true"
PROFILE_VLLM_ASCEND = "vllm-ascend"
STEP_LLM = "llm"
PROFILE_OPENCODE = "opencode"
STEP_DOWNLOAD = "download"


def load_module_from_file(module_name: str, file_path: Path):
    """动态加载模块,避免使用sys.path.insert"""
    spec = importlib.util.spec_from_file_location(module_name, file_path)
    if spec and spec.loader:
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        return module
    return None


class TestJavaScriptSyntax:
    """JavaScript语法检查"""
    
    @staticmethod
    def test_js_files_exist(js_files: List[Path]) -> None:
        """测试JS文件存在"""
        assert len(js_files) > 0, "未找到JavaScript文件"
    
    @pytest.fixture
    def js_files(self) -> List[Path]:
        """获取所有JS文件"""
        js_files = []
        static_dir = WEBUI_DIR / "static"
        if static_dir.exists():
            js_files = list(static_dir.glob("*.js"))
        return js_files
    
    @pytest.mark.parametrize("js_file", [
        WEBUI_DIR / "static" / "app.js",
    ])
    def test_js_file_syntax(self, js_file: Path) -> None:
        """测试JavaScript语法"""
        if not js_file.exists():
            pytest.skip(f"{js_file} 不存在")
        
        content = js_file.read_text()
        
        basic_checks = [
            ("括号匹配", lambda c: c.count("{") == c.count("}")),
            ("圆括号匹配", lambda c: c.count("(") == c.count(")")),
            ("方括号匹配", lambda c: c.count("[") == c.count("]")),
        ]
        
        for check_name, check_func in basic_checks:
            assert check_func(content), f"{js_file.name}: {check_name}失败"
        
        node_path = shutil.which(KEY_NODE)
        if node_path:
            result = subprocess.run(
                [node_path, "--check", str(js_file)],
                capture_output=True,
                text=True
            )
            assert result.returncode == 0, f"Node.js语法检查失败: {result.stderr}"


class TestTemplatesSyntax:
    """Jinja2模板语法检查"""
    
    @staticmethod
    def test_templates_exist(template_files: List[Path]) -> None:
        """测试模板文件存在"""
        assert len(template_files) > 0, "未找到模板文件"
    
    @staticmethod
    def test_template_in_script_block() -> None:
        """测试step.html中的script block语法"""
        step_html = WEBUI_DIR / KEY_TEMPLATES / "step.html"
        if not step_html.exists():
            pytest.skip("step.html不存在")
        
        content = step_html.read_text()
        
        script_start = content.find("{% block scripts %}")
        script_end = content.find("{% endblock %}")
        
        if script_start == -1 or script_end == -1:
            pytest.fail("step.html缺少{% block scripts %}或{% endblock %}")
        
        script_content = content[script_start:script_end]
        
        js_checks = [
            ("括号匹配", lambda c: c.count("{") == c.count("}")),
            ("圆括号匹配", lambda c: c.count("(") == c.count(")")),
            ("方括号匹配", lambda c: c.count("[") == c.count("]")),
        ]
        
        for check_name, check_func in js_checks:
            assert check_func(script_content), f"step.html script block: {check_name}失败"

    @pytest.fixture
    def template_files(self) -> List[Path]:
        """获取所有模板文件"""
        template_dir = WEBUI_DIR / KEY_TEMPLATES
        if template_dir.exists():
            return list(template_dir.glob("*.html"))
        return []
    
    @pytest.mark.parametrize("template_file", [
        WEBUI_DIR / KEY_TEMPLATES / "base.html",
        WEBUI_DIR / KEY_TEMPLATES / "index.html",
        WEBUI_DIR / KEY_TEMPLATES / "workflow.html",
        WEBUI_DIR / KEY_TEMPLATES / "step.html",
        WEBUI_DIR / KEY_TEMPLATES / "logs.html",
        WEBUI_DIR / KEY_TEMPLATES / "tasks.html",
    ])
    def test_template_syntax(self, template_file: Path) -> None:
        """测试Jinja2模板语法"""
        if not template_file.exists():
            pytest.skip(f"{template_file} 不存在")
        
        content = template_file.read_text()
        
        block_checks = [
            ("{% block %}匹配", lambda c: c.count("{% block") == c.count("{% endblock")),
            ("{% for %}匹配", lambda c: c.count("{% for") == c.count("{% endfor")),
            ("{% if %}匹配", lambda c: c.count("{% if") >= c.count("{% endif")),
        ]
        
        for check_name, check_func in block_checks:
            assert check_func(content), f"{template_file.name}: {check_name}失败"
        
        template_errors = []
        STR_JINJA_VAR_END = "}}"
        for i, line in enumerate(content.split("\n"), 1):
            line_stripped = line.strip()
            
            if "{{ " in line and not line.endswith(STR_JINJA_VAR_END) and STR_JINJA_VAR_END not in line:
                if not any(x in line for x in ["{%", "|", "and", "or", "else", "endif", "endfor", "endblock"]):
                    if line_stripped.count("{{") != line_stripped.count(STR_JINJA_VAR_END):
                        template_errors.append(f"行{i}: 双花括号可能未闭合")
            
            if "{% " in line and " %}" in line:
                if "}%" in line and "%}" not in line:
                    template_errors.append(f"行{i}: Jinja2语法错误,可能有错误闭合符号")
        
        assert len(template_errors) == 0, f"{template_file.name}模板语法错误:\n" + "\n".join(template_errors)

class TestPythonSyntax:
    """Python语法检查"""
    
    @staticmethod
    def test_python_files_exist(python_files: List[Path]) -> None:
        """测试Python文件存在"""
        assert len(python_files) > 0, "未找到Python文件"
    
    @staticmethod
    def test_import_app_module() -> None:
        """测试导入app模块"""
        module = load_module_from_file("app", WEBUI_DIR / "app.py")
        if module is None:
            pytest.fail("无法加载app.py")
    
    @staticmethod
    def test_import_script_runner_module() -> None:
        """测试导入script_runner模块"""
        module = load_module_from_file("script_runner", WEBUI_DIR / "script_runner.py")
        if module is None:
            pytest.fail("无法加载script_runner.py")

    @pytest.mark.parametrize("py_file", [
        WEBUI_DIR / "app.py",
        WEBUI_DIR / "script_runner.py",
    ])
    def test_python_syntax_import(self, py_file: Path) -> None:
        """测试Python语法(通过导入)"""
        if not py_file.exists():
            pytest.skip(f"{py_file} 不存在")
        
        import sys
        result = subprocess.run(
            [sys.executable, "-m", "py_compile", str(py_file)],
            capture_output=True,
            text=True
        )
        
        assert result.returncode == 0, f"Python语法错误: {result.stderr}"
    
    @pytest.fixture
    def python_files(self) -> List[Path]:
        """获取所有Python文件"""
        py_files = []
        if WEBUI_DIR.exists():
            py_files = list(WEBUI_DIR.glob("*.py"))
        return py_files

class TestConfigFile:
    """配置文件验证"""
    
    @staticmethod
    def test_config_file_exists() -> None:
        """测试配置文件存在"""
        config_file = WEBUI_DIR / "scripts_config.yaml"
        assert config_file.exists(), f"配置文件不存在: {config_file}"
    
    @staticmethod
    def test_config_yaml_syntax() -> None:
        """测试YAML语法"""
        config_file = WEBUI_DIR / "scripts_config.yaml"
        if not config_file.exists():
            pytest.skip("配置文件不存在")
        
        try:
            with open(config_file, "r", encoding="utf-8") as f:
                config = yaml.safe_load(f)
            assert config is not None, "配置文件为空"
        except yaml.YAMLError as e:
            pytest.fail(f"YAML语法错误: {e}")
    
    @staticmethod
    def test_config_structure() -> None:
        """测试配置文件结构"""
        config_file = WEBUI_DIR / "scripts_config.yaml"
        if not config_file.exists():
            pytest.skip("配置文件不存在")
        
        with open(config_file, "r", encoding="utf-8") as f:
            config = yaml.safe_load(f)
        
        assert KEY_PROFILES in config, "配置缺少profiles字段"
        assert isinstance(config[KEY_PROFILES], dict), "profiles应该是字典"
        
        for profile_name, profile_data in config[KEY_PROFILES].items():
            assert "display_name" in profile_data, f"{profile_name}缺少display_name"
            assert KEY_WORKFLOW in profile_data, f"{profile_name}缺少workflow"
            assert isinstance(profile_data[KEY_WORKFLOW], list), f"{profile_name}的workflow应该是列表"
            
            for step in profile_data[KEY_WORKFLOW]:
                required_fields = [KEY_NAME, "script", "title", "description"]
                for field in required_fields:
                    assert field in step, f"{profile_name}的步骤缺少{field}"
                
                if KEY_PARAMS in step:
                    assert isinstance(step[KEY_PARAMS], list), f"{profile_name}/{step[KEY_NAME]}的params应该是列表"
                    
                    for param in step[KEY_PARAMS]:
                        if param.get(KEY_TYPE) == "help_panel":
                            param_required = [KEY_NAME, KEY_TYPE, "title"]
                        else:
                            param_required = [KEY_NAME, KEY_TYPE, "label"]
                        for field in param_required:
                            assert field in param, f"{profile_name}/{step[KEY_NAME]}的参数缺少{field}"
    
    @staticmethod
    def test_config_scripts_exist() -> None:
        """测试配置中的脚本文件存在"""
        config_file = WEBUI_DIR / "scripts_config.yaml"
        if not config_file.exists():
            pytest.skip("配置文件不存在")
        
        with open(config_file, "r", encoding="utf-8") as f:
            config = yaml.safe_load(f)
        
        for _, profile_data in config[KEY_PROFILES].items():
            for step in profile_data[KEY_WORKFLOW]:
                script_path = PROJECT_ROOT / "kadt" / step["script"]
                assert script_path.exists(), f"脚本不存在: {step['script']}"


class TestFlaskRoutes:
    """Flask路由测试"""
    
    @staticmethod
    def test_index_route(client) -> None:
        """测试首页路由"""
        response = client.get("/")
        assert response.status_code == 200
        assert "text/html" in response.content_type
    
    @staticmethod
    def test_tasks_route(client) -> None:
        """测试任务列表路由"""
        response = client.get("/tasks")
        assert response.status_code == 200
    
    @staticmethod
    def test_profile_route(client) -> None:
        """测试profile详情路由"""
        profiles = [PROFILE_OPENCODE, "openclaw", PROFILE_VLLM_ASCEND]
        for profile in profiles:
            response = client.get(f"/profile/{profile}")
            assert response.status_code == 200, f"profile/{profile}返回{response.status_code}"
    
    @staticmethod
    def test_step_route(client) -> None:
        """测试步骤页面路由"""
        test_cases = [
            (PROFILE_OPENCODE, STEP_DOWNLOAD),
            (PROFILE_VLLM_ASCEND, STEP_LLM),
        ]
        for profile, step in test_cases:
            response = client.get(f"/step/{profile}/{step}")
            assert response.status_code == 200, f"/step/{profile}/{step}返回{response.status_code}"
    
    @staticmethod
    def test_api_profiles(client) -> None:
        """测试API: 获取profiles"""
        response = client.get("/api/profiles")
        assert response.status_code == 200
        data = json.loads(response.data)
        assert isinstance(data, list)
    
    @staticmethod
    def test_api_workflow(client) -> None:
        """测试API: 获取workflow"""
        response = client.get("/api/workflow/opencode")
        assert response.status_code == 200
        data = json.loads(response.data)
        assert isinstance(data, list)
    
    @staticmethod
    def test_api_health(client) -> None:
        """测试API: 健康检查"""
        response = client.get("/api/health")
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data.get("status") == "ok"

    @pytest.fixture
    def client(self):
        """获取Flask测试客户端"""
        app_module = load_module_from_file("app", WEBUI_DIR / "app.py")
        if app_module is None:
            pytest.skip("无法加载Flask应用")
        
        flask_app = app_module.app
        flask_app.testing = True
        yield flask_app.test_client()

class TestScriptRunner:
    """ScriptRunner功能测试"""
    
    @staticmethod
    def test_get_profiles(runner) -> None:
        """测试获取profiles"""
        profiles = runner.get_profiles()
        assert isinstance(profiles, list)
        assert len(profiles) > 0
        
        for profile in profiles:
            assert KEY_NAME in profile
            assert "display_name" in profile
    
    @staticmethod
    def test_get_workflow(runner) -> None:
        """测试获取workflow"""
        workflow = runner.get_workflow(PROFILE_OPENCODE)
        assert isinstance(workflow, list)
        assert len(workflow) > 0
        
        for step in workflow:
            assert KEY_NAME in step
            assert "script" in step
    
    @staticmethod
    def test_get_step(runner) -> None:
        """测试获取单个步骤"""
        step = runner.get_step(PROFILE_OPENCODE, STEP_DOWNLOAD)
        assert step is not None
        assert step.get(KEY_NAME) == STEP_DOWNLOAD
    
    @staticmethod
    def test_build_command(runner) -> None:
        """测试构建命令"""
        params = {"model": "Qwen3.5-27B-w8a8-mtp", "os": "openEuler22.03"}
        cmd = runner.build_command(PROFILE_OPENCODE, STEP_DOWNLOAD, params)
        assert isinstance(cmd, list)
        assert cmd[0] == "bash"
        assert "--os" in cmd
    
    @staticmethod
    def test_checkbox_command(runner) -> None:
        """测试布尔select参数构建(bool_flag区分)"""
        params = {"model": "Qwen3.5-27B-w8a8-mtp", "port": "8000", "api-key": "test123"}
        
        params["dual-node"] = KEY_FALSE
        cmd = runner.build_command(PROFILE_VLLM_ASCEND, STEP_LLM, params)
        assert "--dual-node" not in cmd, "dual-node为false时不应该添加参数"
        
        params["dual-node"] = KEY_TRUE
        cmd = runner.build_command(PROFILE_VLLM_ASCEND, STEP_LLM, params)
        assert "--dual-node" in cmd, "dual-node为true时应该添加参数"
        assert KEY_TRUE not in cmd, "bool_flag参数不应该添加值"
        
        params["privileged"] = KEY_FALSE
        cmd = runner.build_command(PROFILE_VLLM_ASCEND, STEP_LLM, params)
        assert "--privileged" in cmd, "privileged参数应该添加"
        assert KEY_FALSE in cmd, "非bool_flag参数应该添加值"
        
        params["privileged"] = KEY_TRUE
        cmd = runner.build_command(PROFILE_VLLM_ASCEND, STEP_LLM, params)
        assert "--privileged" in cmd
        assert KEY_TRUE in cmd, "privileged=true时应该传值true"
    
    @staticmethod
    def test_inventory_update(runner) -> None:
        """测试inventory更新"""
        inventory_file = PROJECT_ROOT / "ascend_deployer" / "inventory_file"
        if not inventory_file.exists():
            pytest.skip("inventory_file不存在")
        
        original_content = inventory_file.read_text()
        
        try:
            servers = [
                {"ip": "10.10.10.1", "pass": "test123"},
                {"ip": "10.10.10.2", "pass": "test456"},
            ]
            result = runner.update_inventory(servers)
            assert result is True
            
            new_content = inventory_file.read_text()
            assert "10.10.10.1" in new_content
            assert "10.10.10.2" in new_content
            assert "ansible_ssh_user=\"root\"" in new_content
            assert "ansible_ssh_pass=\"test123\"" in new_content
            
            result_empty = runner.update_inventory([])
            assert result_empty is True
            
            default_content = inventory_file.read_text()
            assert "localhost ansible_connection='local'" in default_content
            
        finally:
            inventory_file.write_text(original_content)
    
    @pytest.fixture
    def runner(self):
        """获取ScriptRunner实例"""
        script_runner_module = load_module_from_file("script_runner", WEBUI_DIR / "script_runner.py")
        if script_runner_module is None:
            pytest.skip("无法加载ScriptRunner")
        
        ScriptRunner = script_runner_module.ScriptRunner
        config_path = WEBUI_DIR / "scripts_config.yaml"
        runner = ScriptRunner(str(config_path), base_dir=str(PROJECT_ROOT))
        yield runner


class TestStaticFiles:
    """静态文件测试"""
    
    @staticmethod
    def test_css_file_exists() -> None:
        """测试CSS文件存在"""
        css_file = WEBUI_DIR / "static" / "style.css"
        assert css_file.exists(), "style.css不存在"
    
    @staticmethod
    def test_css_syntax() -> None:
        """测试CSS基本语法"""
        css_file = WEBUI_DIR / "static" / "style.css"
        if not css_file.exists():
            pytest.skip("style.css不存在")
        
        content = css_file.read_text()
        
        assert content.count("{") == content.count("}"), "CSS括号不匹配"


class TestHTMLRendering:
    """HTML渲染测试 - 验证关键元素正确渲染"""
    
    @staticmethod
    def test_json_data_attributes_quote_format(app_client) -> None:
        """测试JSON数据属性使用单引号包裹(避免引号冲突)"""
        resp = app_client.get(f'/step/{PROFILE_OPENCODE}/{STEP_LLM}')
        html = resp.data.decode('utf-8')
        
        assert "data-filter-depends='[" in html, \
            "data-filter-depends应该使用单引号包裹JSON数组"
        
        assert "data-filter-rules='{" in html, \
            "data-filter-rules应该使用单引号包裹JSON对象"
        
        assert "data-filter-depends=\"[" not in html, \
            "data-filter-depends不应该使用双引号(会与JSON内部引号冲突)"
        assert "data-filter-rules=\"{" not in html, \
            "data-filter-rules不应该使用双引号(会与JSON内部引号冲突)"
    
    @staticmethod
    def test_help_panel_rendering(app_client) -> None:
        """测试help_panel正确渲染"""
        resp = app_client.get(f'/step/{PROFILE_OPENCODE}/{STEP_LLM}')
        html = resp.data.decode('utf-8')
        
        assert 'help-panel-title' in html, "help_panel应该有标题元素"
        assert '模型兼容性说明' in html, "help_panel标题内容应该正确"
        
        assert 'help-panel-content' in html, "help_panel应该有内容区域"
        assert 'help-section-title' in html, "help_panel应该有分节标题"
        
        assert 'help-model-tag' in html, "help_panel应该显示模型标签"
    
    @staticmethod
    def test_dynamic_filter_attributes(app_client) -> None:
        """测试dynamic_filter参数正确渲染所有必需属性"""
        resp = app_client.get(f'/step/{PROFILE_OPENCODE}/{STEP_LLM}')
        html = resp.data.decode('utf-8')
        
        assert 'data-dynamic-filter="true"' in html, \
            "model select应该有data-dynamic-filter属性"
        
        assert '"server-type"' in html, "filter_depends应该包含server-type"
        assert '"dual-node"' in html, "filter_depends应该包含dual-node"
        
        assert 'a2_single' in html, "filter_rules应该包含a2_single键"
        assert 'a2_multi' in html, "filter_rules应该包含a2_multi键"
        assert 'a3_single' in html, "filter_rules应该包含a3_single键"
        assert 'a3_multi' in html, "filter_rules应该包含a3_multi键"
    
    @staticmethod
    def test_default_values_rendering(app_client) -> None:
        """测试参数默认值正确渲染"""
        resp = app_client.get(f'/step/{PROFILE_OPENCODE}/{STEP_LLM}')
        html = resp.data.decode('utf-8')
        
        assert 'data-default="a2"' in html, \
            "server-type应该有data-default='a2'"
        
        assert 'data-default="false"' in html, \
            "dual-node应该有data-default='false'"
        
        assert '<option value="a2"' in html and 'selected' in html, \
            "server-type的a2选项应该默认选中"
        assert '<option value="false"' in html and 'selected' in html, \
            "dual-node的false选项应该默认选中"
    
    @staticmethod
    def test_json_parseable_in_html(app_client) -> None:
        """测试HTML中的JSON可以被正确解析"""
        resp = app_client.get(f'/step/{PROFILE_OPENCODE}/{STEP_LLM}')
        html = resp.data.decode('utf-8')
        
        import re
        deps_pattern = r"data-filter-depends='([^']+)'"
        deps_match = re.search(deps_pattern, html)
        if deps_match:
            deps_json = deps_match.group(1)
            try:
                deps = json.loads(deps_json)
                assert isinstance(deps, list), "filter_depends应该是列表"
                assert "server-type" in deps, "filter_depends应该包含server-type"
                assert "dual-node" in deps, "filter_depends应该包含dual-node"
            except json.JSONDecodeError as e:
                pytest.fail(f"data-filter-depends的JSON无法解析: {e}")
        
        rules_pattern = r"data-filter-rules='([^']+)'"
        rules_match = re.search(rules_pattern, html)
        if rules_match:
            rules_json = rules_match.group(1)
            try:
                rules = json.loads(rules_json)
                assert isinstance(rules, dict), "filter_rules应该是字典"
                assert "a2_single" in rules, "filter_rules应该包含a2_single"
                assert "a2_multi" in rules, "filter_rules应该包含a2_multi"
            except json.JSONDecodeError as e:
                pytest.fail(f"data-filter-rules的JSON无法解析: {e}")
    
    @pytest.fixture
    def app_client(self):
        """创建Flask测试客户端"""
        app_module = load_module_from_file("app", WEBUI_DIR / "app.py")
        if app_module is None:
            pytest.skip("无法加载Flask应用")
        
        app = app_module.app
        app.testing = True
        with app.test_client() as client:
            yield client


if __name__ == "__main__":
    pytest.main([__file__, "-v"])