# -*- coding: utf-8 -*-
"""ContextAttachmentResolver @file:L10-L20 引用解析测试（Phase 7 Tier 1 BD）。

借鉴 zhikuncode ContextAttachmentResolver.java + IDE 引用语法：
- @file:L10-L20       引用第 10-20 行
- @file:L10           引用第 10 行
- @file               引用整个文件（受最大行数限制）
- @file:func:foo      引用名为 foo 的函数（可选，基础版可跳过）
"""
from pathlib import Path

import pytest

from abyss.context.attachment_resolver import (
    ContextAttachmentResolver,
    Attachment,
    get_default_resolver,
)


@pytest.fixture
def resolver(tmp_path: Path) -> ContextAttachmentResolver:
    """返回独立 resolver 实例，工作目录设为 tmp_path。"""
    return ContextAttachmentResolver(working_dir=str(tmp_path))


@pytest.fixture
def make_file(tmp_path: Path):
    def _make(name: str, content: str = "hello") -> str:
        p = tmp_path / name
        p.write_text(content, encoding="utf-8")
        return name  # 返回相对路径
    return _make


class TestParseReferences:
    """引用语法解析测试。"""

    def test_parse_single_line_ref(self, resolver):
        text = "看下 @file.py:L10"
        refs = resolver.parse_references(text)
        assert len(refs) == 1
        assert refs[0].path == "file.py"
        assert refs[0].start_line == 10
        assert refs[0].end_line == 10

    def test_parse_line_range_ref(self, resolver):
        text = "@src/main.py:L10-L20"
        refs = resolver.parse_references(text)
        assert len(refs) == 1
        assert refs[0].path == "src/main.py"
        assert refs[0].start_line == 10
        assert refs[0].end_line == 20

    def test_parse_whole_file_ref(self, resolver):
        text = "@src/main.py"
        refs = resolver.parse_references(text)
        assert len(refs) == 1
        assert refs[0].path == "src/main.py"
        assert refs[0].start_line is None
        assert refs[0].end_line is None

    def test_parse_multiple_refs(self, resolver):
        text = "对比 @a.py:L1-L10 和 @b.py:L20"
        refs = resolver.parse_references(text)
        assert len(refs) == 2
        assert refs[0].path == "a.py"
        assert refs[1].path == "b.py"

    def test_parse_no_refs(self, resolver):
        text = "没有引用的普通消息"
        refs = resolver.parse_references(text)
        assert refs == []

    def test_parse_email_not_treated_as_ref(self, resolver):
        """邮箱不应被识别为引用。"""
        text = "联系 abc@example.com"
        refs = resolver.parse_references(text)
        # 邮箱格式 user@domain，但路径不含 .py/.js 等
        # 至少不应误识别 example.com 为路径
        assert all(r.path != "example.com" for r in refs)

    def test_parse_ref_in_chinese_text(self, resolver):
        text = "查看 @file.py:L5-L15 的实现"
        refs = resolver.parse_references(text)
        assert len(refs) == 1
        assert refs[0].path == "file.py"


class TestResolve:
    """引用展开测试。"""

    def test_resolve_whole_file(self, resolver, make_file):
        make_file("file.py", "line1\nline2\nline3\n")
        text = "查看 @file.py"
        result = resolver.resolve(text)
        assert "line1" in result
        assert "line2" in result
        assert "line3" in result

    def test_resolve_line_range(self, resolver, make_file):
        make_file("file.py", "L1\nL2\nL3\nL4\nL5\n")
        text = "看 @file.py:L2-L4"
        result = resolver.resolve(text)
        assert "L2" in result
        assert "L3" in result
        assert "L4" in result
        assert "L1" not in result
        assert "L5" not in result

    def test_resolve_single_line(self, resolver, make_file):
        make_file("file.py", "L1\nL2\nL3\n")
        text = "@file.py:L2"
        result = resolver.resolve(text)
        assert "L2" in result
        # 其他行不在结果中
        assert "L1" not in result
        assert "L3" not in result

    def test_resolve_multiple_refs(self, resolver, make_file):
        make_file("a.py", "A1\nA2\n")
        make_file("b.py", "B1\nB2\n")
        text = "对比 @a.py:L1 和 @b.py:L2"
        result = resolver.resolve(text)
        assert "A1" in result
        assert "B2" in result

    def test_resolve_nonexistent_file(self, resolver):
        text = "看 @nonexistent.py:L1-L10"
        result = resolver.resolve(text)
        # 不存在文件应保留原引用，并附错误说明
        assert "nonexistent.py" in result
        assert "不存在" in result or "ERR" in result or "未找到" in result

    def test_resolve_out_of_range_lines(self, resolver, make_file):
        make_file("file.py", "L1\nL2\n")
        text = "@file.py:L1-L100"
        result = resolver.resolve(text)
        # 超出范围的行应只返回实际存在的行
        assert "L1" in result
        assert "L2" in result

    def test_resolve_preserves_original_text(self, resolver, make_file):
        make_file("file.py", "content\n")
        text = "请检查 @file.py 这段代码"
        result = resolver.resolve(text)
        assert "请检查" in result
        assert "这段代码" in result

    def test_resolve_max_lines_truncation(self, resolver, make_file):
        """大文件应被截断。"""
        make_file("big.py", "".join(f"line{i}\n" for i in range(1000)))
        text = "@big.py"
        result = resolver.resolve(text)
        # 默认最大 500 行
        assert "截断" in result or "..." in result


class TestFormatAttachment:
    """格式化输出测试。"""

    def test_format_with_filename_header(self, resolver, make_file):
        make_file("file.py", "code\n")
        text = "@file.py"
        result = resolver.resolve(text)
        # 应包含文件名头部
        assert "file.py" in result
        # 应使用代码块格式
        assert "```" in result

    def test_format_with_line_numbers(self, resolver, make_file):
        make_file("file.py", "L1\nL2\nL3\n")
        text = "@file.py:L2-L3"
        result = resolver.resolve(text)
        # 应包含行号信息
        assert "L2" in result
        assert "L3" in result
        # 应标识行范围
        assert "L2-L3" in result or "2-3" in result or "L2" in result


class TestDefaultResolver:
    """模块级单例测试。"""

    def test_get_default_returns_same_instance(self):
        r1 = get_default_resolver()
        r2 = get_default_resolver()
        assert r1 is r2

    def test_default_resolver_can_set_working_dir(self, tmp_path):
        r = get_default_resolver()
        r.set_working_dir(str(tmp_path))
        assert r._working_dir == str(tmp_path)
        # 清理
        r.set_working_dir(None)


class TestEdgeCases:
    """边缘情况测试：覆盖 CLI 真实使用中的边界场景。"""

    def test_resolve_path_traversal_rejected(self, resolver, tmp_path):
        """路径穿越应被拒绝。"""
        # 创建一个工作目录外的文件
        outside = tmp_path.parent / "outside_secret.py"
        outside.write_text("secret\n", encoding="utf-8")
        try:
            # resolver 工作目录为 tmp_path，穿越到父目录应被拒绝
            resolver.set_working_dir(str(tmp_path))
            # 设置 PathSecurityService 工作目录
            from abyss.security.path_security import get_default_service
            get_default_service().set_working_dir(str(tmp_path))
            text = f"看 @{outside.name}"
            result = resolver.resolve(text)
            # 应该提示路径不安全
            assert "ERR" in result or "不安全" in result
        finally:
            outside.unlink(missing_ok=True)

    def test_resolve_special_chars_in_path(self, resolver, make_file):
        """路径含特殊字符（空格、连字符）应能解析。"""
        # 注意：当前正则不支持空格，但支持连字符
        make_file("my-file.py", "code\n")
        text = "@my-file.py"
        result = resolver.resolve(text)
        assert "code" in result

    def test_resolve_chinese_filename(self, resolver, make_file):
        """中文文件名不支持（正则限制 A-Za-z0-9），但不应崩溃。"""
        # 当前正则 [A-Za-z0-9_\-./\\]+ 不匹配中文字符
        # 因此中文文件名不会被识别为引用，原文保留
        text = "@中文文件.py"
        result = resolver.resolve(text)
        # 不应崩溃，原文保留
        assert "中文文件" in result

    def test_resolve_empty_file(self, resolver, make_file):
        """空文件应能正确处理。"""
        make_file("empty.py", "")
        text = "@empty.py"
        result = resolver.resolve(text)
        # 应包含文件名头部，代码块为空
        assert "empty.py" in result
        assert "```" in result

    def test_resolve_binary_file_fallback(self, resolver, tmp_path):
        """二进制文件用 errors='replace' 读取，不应崩溃。"""
        # 直接用绝对路径写入二进制内容
        p = tmp_path / "binary.dat"
        p.write_bytes(b"\x80\x81\xff\x00 binary content")
        text = f"@{p.name}"
        result = resolver.resolve(text)
        # 应该能读取（用 replace 模式），或返回 ERR
        assert "binary content" in result or "ERR" in result

    def test_resolve_single_line_format(self, resolver, make_file):
        """单行引用应显示为 :L10 而非 :L10-L10。"""
        make_file("file.py", "L1\nL2\nL3\n")
        text = "@file.py:L2"
        result = resolver.resolve(text)
        # 头部应为 📎 file.py:L2
        assert "📎" in result
        assert ":L2" in result
        # 不应出现冗余的 :L2-L2
        assert ":L2-L2" not in result

    def test_resolve_range_format(self, resolver, make_file):
        """范围引用应显示为 :L10-L20。"""
        make_file("file.py", "".join(f"line{i}\n" for i in range(1, 30)))
        text = "@file.py:L10-L20"
        result = resolver.resolve(text)
        assert "📎" in result
        assert ":L10-L20" in result

    def test_resolve_inverted_range_clamped(self, resolver, make_file):
        """end < start 时应正确截取（start 到 end 之间）。"""
        make_file("file.py", "L1\nL2\nL3\nL4\nL5\n")
        # 不正常的范围：start=4, end=2
        # 当前实现用切片 [start-1:end]，所以会得到空片段
        # 但不应崩溃
        text = "@file.py:L4-L2"
        result = resolver.resolve(text)
        # 不崩溃即可
        assert "file.py" in result

    def test_resolve_very_long_file_truncates(self, resolver, make_file):
        """超过 max_lines 的文件应被截断并提示。"""
        make_file("huge.py", "".join(f"line{i}\n" for i in range(2000)))
        text = "@huge.py"
        result = resolver.resolve(text)
        assert "截断" in result
        assert "500" in result
        assert "2000" in result

    def test_resolve_max_lines_zero_means_no_truncation(self, tmp_path):
        """max_lines 设为极大值应不截断。"""
        resolver = ContextAttachmentResolver(
            working_dir=str(tmp_path),
            max_lines=10**9,
        )
        (tmp_path / "file.py").write_text("".join(f"line{i}\n" for i in range(100)), encoding="utf-8")
        text = "@file.py"
        result = resolver.resolve(text)
        assert "截断" not in result

    def test_resolve_preserves_text_without_refs(self, resolver):
        """无引用的文本应原样返回。"""
        text = "这是一条普通消息，没有任何文件引用"
        result = resolver.resolve(text)
        assert result == text

    def test_resolve_preserves_arobase_in_text(self, resolver):
        """单独的 @ 字符不应被识别为引用。"""
        text = "我的邮箱是 user@host 但这里没有文件"
        result = resolver.resolve(text)
        assert result == text

    def test_resolve_handles_multiple_same_file_refs(self, resolver, make_file):
        """同一文件多次引用应分别展开。"""
        make_file("file.py", "L1\nL2\nL3\nL4\nL5\n")
        text = "对比 @file.py:L1 和 @file.py:L5"
        result = resolver.resolve(text)
        assert "L1" in result
        assert "L5" in result

    def test_resolve_preserves_surrounding_text(self, resolver, make_file):
        """引用前后的文本应被保留。"""
        make_file("file.py", "code\n")
        text = "请帮我检查 @file.py 这个文件的逻辑"
        result = resolver.resolve(text)
        assert "请帮我检查" in result
        assert "这个文件的逻辑" in result
