"""
文件选择与排除模式 - 端到端 API 测试

此脚本测试完整的 API 流程：
1. 创建测试项目
2. 上传 ZIP 文件
3. 获取文件列表（带/不带排除模式）
4. 启动扫描任务（带排除模式和文件选择）

使用方法：
    python tests/test_file_selection_e2e.py

环境要求：
    - 后端服务运行在 http://localhost:8000
    - 需要有效的用户认证 token
"""

import httpx
import json
import os
import sys
import tempfile
import zipfile
import time
from pathlib import Path

# 配置 - 使用 127.0.0.1 避免 IPv6 问题
BASE_URL = os.getenv("API_BASE_URL", "http://127.0.0.1:8000/api/v1")
# 演示账户
DEMO_EMAIL = "demo@example.com"
DEMO_PASSWORD = "demo123"
# 如果没有设置 token，会尝试用演示账户登录
AUTH_TOKEN = os.getenv("AUTH_TOKEN", "")

# 测试数据
TEST_FILES = {
    "src/main.py": '''
def main():
    password = "admin123"  # 硬编码密码
    print("Hello World")

if __name__ == "__main__":
    main()
''',
    "src/utils.py": '''
def helper():
    return "helper"
''',
    "src/tests/test_main.py": '''
def test_main():
    assert True
''',
    "node_modules/lib.js": '''
module.exports = {};
''',
    "dist/bundle.js": '''
var a = 1;
''',
    ".git/config": '''
[core]
    repositoryformatversion = 0
''',
    "app.log": '''
2024-01-01 INFO: Application started
''',
    "README.md": '''
# Test Project
This is a test project.
''',
}


def create_test_zip() -> str:
    """创建测试 ZIP 文件"""
    temp_dir = tempfile.mkdtemp()
    zip_path = os.path.join(temp_dir, "test_project.zip")
    
    with zipfile.ZipFile(zip_path, 'w') as zf:
        for filename, content in TEST_FILES.items():
            zf.writestr(filename, content)
    
    print(f"✅ 创建测试 ZIP 文件: {zip_path}")
    return zip_path


def get_headers(token: str = None):
    """获取请求头"""
    headers = {"Content-Type": "application/json"}
    t = token or AUTH_TOKEN
    if t:
        headers["Authorization"] = f"Bearer {t}"
    return headers


def login_demo_account() -> str | None:
    """使用演示账户登录获取 token"""
    try:
        with httpx.Client(timeout=10.0, proxy=None, trust_env=False) as client:
            response = client.post(
                f"{BASE_URL}/auth/login",
                data={
                    "username": DEMO_EMAIL,
                    "password": DEMO_PASSWORD,
                },
                headers={"Content-Type": "application/x-www-form-urlencoded"}
            )
            if response.status_code == 200:
                data = response.json()
                return data.get("access_token")
            else:
                print(f"⚠️ 登录失败: {response.status_code} - {response.text}")
    except Exception as e:
        print(f"⚠️ 登录请求失败: {e}")
    return None


class FileSelectionE2ETest:
    """端到端测试类"""
    
    def __init__(self):
        # 禁用环境代理设置，避免 502 错误
        self.client = httpx.Client(timeout=30.0, proxy=None, trust_env=False)
        self.project_id = None
        self.zip_path = None
        self.token = AUTH_TOKEN
    
    def cleanup(self):
        """清理测试资源"""
        if self.zip_path and os.path.exists(self.zip_path):
            os.remove(self.zip_path)
            os.rmdir(os.path.dirname(self.zip_path))
            print("✅ 清理临时文件")
        
        if self.project_id:
            try:
                self.client.delete(
                    f"{BASE_URL}/projects/{self.project_id}",
                    headers=get_headers(self.token)
                )
                print(f"✅ 删除测试项目: {self.project_id}")
            except Exception as e:
                print(f"⚠️ 删除项目失败: {e}")
        
        self.client.close()
    
    def test_health_check(self) -> bool:
        """测试服务健康状态并登录"""
        print("\n[测试] 服务健康检查...")
        
        # 尝试访问健康检查端点
        # BASE_URL 是 http://localhost:8000/api/v1，需要去掉 /api/v1
        base = BASE_URL.rsplit('/api/v1', 1)[0]
        health_url = f"{base}/health"
        print(f"  健康检查 URL: {health_url}")
        
        try:
            response = self.client.get(health_url)
            print(f"  响应状态: {response.status_code}")
            if response.status_code == 200:
                print(f"✅ 服务运行正常")
            else:
                print(f"⚠️ 健康检查返回: {response.status_code} - {response.text}")
                return False
        except Exception as e:
            print(f"❌ 无法连接服务: {e}")
            return False
        
        # 如果没有 token，尝试用演示账户登录
        if not self.token:
            print("\n[测试] 使用演示账户登录...")
            self.token = login_demo_account()
            if self.token:
                print(f"✅ 登录成功，获取到 token")
            else:
                print("❌ 登录失败，无法继续测试")
                return False
        
        return True
    
    def test_create_project(self) -> bool:
        """测试创建 ZIP 项目"""
        print("\n[测试] 创建 ZIP 项目...")
        
        project_data = {
            "name": f"Test Project {int(time.time())}",
            "description": "文件选择功能测试项目",
            "source_type": "zip",
        }
        
        try:
            response = self.client.post(
                f"{BASE_URL}/projects/",
                json=project_data,
                headers=get_headers(self.token)
            )
            
            if response.status_code == 200:
                data = response.json()
                self.project_id = data.get("id")
                print(f"✅ 项目创建成功: {self.project_id}")
                return True
            elif response.status_code == 401:
                print("⚠️ 需要认证，跳过此测试")
                return False
            else:
                print(f"❌ 创建项目失败: {response.status_code} - {response.text}")
                return False
        except Exception as e:
            print(f"❌ 请求失败: {e}")
            return False
    
    def test_upload_zip(self) -> bool:
        """测试上传 ZIP 文件"""
        if not self.project_id:
            print("⚠️ 跳过：没有项目 ID")
            return False
        
        print("\n[测试] 上传 ZIP 文件...")
        
        self.zip_path = create_test_zip()
        
        try:
            with open(self.zip_path, 'rb') as f:
                files = {"file": ("test_project.zip", f, "application/zip")}
                headers = {}
                if self.token:
                    headers["Authorization"] = f"Bearer {self.token}"
                
                response = self.client.post(
                    f"{BASE_URL}/projects/{self.project_id}/zip",
                    files=files,
                    headers=headers
                )
            
            if response.status_code == 200:
                print("✅ ZIP 文件上传成功")
                return True
            else:
                print(f"❌ 上传失败: {response.status_code} - {response.text}")
                return False
        except Exception as e:
            print(f"❌ 请求失败: {e}")
            return False
    
    def test_get_files_without_exclude(self) -> bool:
        """测试获取文件列表（无排除模式）"""
        if not self.project_id:
            print("⚠️ 跳过：没有项目 ID")
            return False
        
        print("\n[测试] 获取文件列表（无排除模式）...")
        
        try:
            response = self.client.get(
                f"{BASE_URL}/projects/{self.project_id}/files",
                headers=get_headers(self.token)
            )
            
            if response.status_code == 200:
                files = response.json()
                print(f"✅ 获取到 {len(files)} 个文件")
                
                # 验证默认排除生效
                paths = [f["path"] for f in files]
                
                # 应该包含的文件
                expected_included = ["src/main.py", "src/utils.py"]
                for path in expected_included:
                    if path in paths:
                        print(f"  ✓ 包含: {path}")
                    else:
                        print(f"  ✗ 缺少: {path}")
                
                # 应该被排除的文件
                expected_excluded = ["node_modules/lib.js", "dist/bundle.js", ".git/config"]
                for path in expected_excluded:
                    if path not in paths:
                        print(f"  ✓ 已排除: {path}")
                    else:
                        print(f"  ✗ 未排除: {path}")
                
                return True
            else:
                print(f"❌ 获取失败: {response.status_code} - {response.text}")
                return False
        except Exception as e:
            print(f"❌ 请求失败: {e}")
            return False
    
    def test_get_files_with_exclude(self) -> bool:
        """测试获取文件列表（带排除模式）"""
        if not self.project_id:
            print("⚠️ 跳过：没有项目 ID")
            return False
        
        print("\n[测试] 获取文件列表（带自定义排除模式）...")
        
        # 自定义排除模式：排除测试文件和日志（使用路径片段匹配）
        exclude_patterns = [".log", "tests/", "test_"]
        
        try:
            response = self.client.get(
                f"{BASE_URL}/projects/{self.project_id}/files",
                params={"exclude_patterns": json.dumps(exclude_patterns)},
                headers=get_headers(self.token)
            )
            
            if response.status_code == 200:
                files = response.json()
                print(f"✅ 获取到 {len(files)} 个文件（应用自定义排除）")
                
                paths = [f["path"] for f in files]
                
                # 验证自定义排除生效
                if "app.log" not in paths:
                    print("  ✓ 已排除: app.log (*.log 模式)")
                else:
                    print("  ✗ 未排除: app.log")
                
                # 检查测试文件是否被排除
                test_files = [p for p in paths if "test" in p.lower()]
                if not test_files:
                    print("  ✓ 已排除所有测试文件")
                else:
                    print(f"  ⚠️ 仍包含测试文件: {test_files}")
                
                return True
            else:
                print(f"❌ 获取失败: {response.status_code} - {response.text}")
                return False
        except Exception as e:
            print(f"❌ 请求失败: {e}")
            return False
    
    def test_scan_with_file_selection(self) -> bool:
        """测试带文件选择的扫描"""
        if not self.project_id:
            print("⚠️ 跳过：没有项目 ID")
            return False
        
        print("\n[测试] 启动扫描（带文件选择和排除模式）...")
        
        scan_request = {
            "file_paths": ["src/main.py"],  # 只扫描一个文件
            "exclude_patterns": [".log", "tests/"],  # 使用路径片段匹配
            "full_scan": False,
        }
        
        try:
            response = self.client.post(
                f"{BASE_URL}/scan/scan-stored-zip",
                params={"project_id": self.project_id},
                json=scan_request,
                headers=get_headers(self.token)
            )
            
            if response.status_code == 200:
                data = response.json()
                task_id = data.get("task_id")
                print(f"✅ 扫描任务已创建: {task_id}")
                return True
            elif response.status_code == 400:
                print(f"⚠️ 扫描请求被拒绝（可能没有存储的 ZIP）: {response.text}")
                return False
            else:
                print(f"❌ 扫描失败: {response.status_code} - {response.text}")
                return False
        except Exception as e:
            print(f"❌ 请求失败: {e}")
            return False


def run_mock_tests():
    """运行模拟测试（不需要真实服务）"""
    print("\n" + "=" * 60)
    print("模拟测试模式（不连接真实服务）")
    print("=" * 60)
    
    # 测试 1: 排除模式参数格式
    print("\n[模拟测试 1] 排除模式参数格式...")
    exclude_patterns = ["node_modules/**", "*.log", "dist/**"]
    json_str = json.dumps(exclude_patterns)
    parsed = json.loads(json_str)
    assert parsed == exclude_patterns
    print(f"✅ JSON 序列化正确: {json_str}")
    
    # 测试 2: 扫描请求格式
    print("\n[模拟测试 2] 扫描请求格式...")
    scan_request = {
        "file_paths": ["src/main.py", "src/utils.py"],
        "exclude_patterns": ["*.test.js", "coverage/**"],
        "full_scan": False,
        "rule_set_id": None,
        "prompt_template_id": None,
    }
    json_str = json.dumps(scan_request)
    parsed = json.loads(json_str)
    assert "exclude_patterns" in parsed
    assert parsed["full_scan"] is False
    print(f"✅ 扫描请求格式正确")
    
    # 测试 3: ZIP 文件创建和读取
    print("\n[模拟测试 3] ZIP 文件处理...")
    zip_path = create_test_zip()
    
    with zipfile.ZipFile(zip_path, 'r') as zf:
        file_list = zf.namelist()
        print(f"✅ ZIP 包含 {len(file_list)} 个文件")
        
        # 验证文件存在
        assert "src/main.py" in file_list
        assert "node_modules/lib.js" in file_list
    
    # 清理
    os.remove(zip_path)
    os.rmdir(os.path.dirname(zip_path))
    print("✅ 清理完成")
    
    print("\n" + "=" * 60)
    print("🎉 所有模拟测试通过！")
    print("=" * 60)


def run_e2e_tests():
    """运行端到端测试"""
    print("\n" + "=" * 60)
    print("端到端 API 测试")
    print("=" * 60)
    print(f"API 地址: {BASE_URL}")
    print(f"认证状态: {'已配置' if AUTH_TOKEN else '未配置'}")
    
    test = FileSelectionE2ETest()
    results = []
    
    try:
        # 健康检查
        if not test.test_health_check():
            print("\n⚠️ 服务不可用，切换到模拟测试模式")
            run_mock_tests()
            return
        
        # 运行测试
        results.append(("创建项目", test.test_create_project()))
        results.append(("上传 ZIP", test.test_upload_zip()))
        results.append(("获取文件（无排除）", test.test_get_files_without_exclude()))
        results.append(("获取文件（带排除）", test.test_get_files_with_exclude()))
        results.append(("扫描（带文件选择）", test.test_scan_with_file_selection()))
        
    finally:
        test.cleanup()
    
    # 打印结果
    print("\n" + "=" * 60)
    print("测试结果汇总")
    print("=" * 60)
    
    passed = 0
    failed = 0
    skipped = 0
    
    for name, result in results:
        if result is True:
            status = "✅ 通过"
            passed += 1
        elif result is False:
            status = "❌ 失败"
            failed += 1
        else:
            status = "⚠️ 跳过"
            skipped += 1
        print(f"  {name}: {status}")
    
    print(f"\n总计: {passed} 通过, {failed} 失败, {skipped} 跳过")
    
    if failed == 0:
        print("\n🎉 所有测试通过！")
    else:
        print("\n⚠️ 部分测试失败，请检查日志")


if __name__ == "__main__":
    # 检查命令行参数
    if len(sys.argv) > 1 and sys.argv[1] == "--mock":
        run_mock_tests()
    else:
        run_e2e_tests()
