# -*- coding: utf-8 -*-
"""AtomicFileWriter 原子文件写入器测试（Phase 6 Tier 1-3 AO）。

借鉴 zhikuncode-main AtomicFileWriter.java：
- 临时文件 → fsync → os.replace（CAS 原子替换）
- 大文件（>10MB）跳过原子写入
- 失败时清理临时文件
"""
import os
import threading
import time
from pathlib import Path
from unittest import mock

import pytest

from abyss.tools.file_io import AtomicFileWriter


@pytest.fixture
def tmp_target(tmp_path: Path) -> str:
    """返回测试用临时文件路径字符串。"""
    return str(tmp_path / "target.txt")


@pytest.fixture
def tmp_dir(tmp_path: Path) -> str:
    """返回测试用临时目录路径字符串。"""
    return str(tmp_path)


class TestAtomicFileWriterBasic:
    """基本写入功能测试。"""

    def test_write_creates_file_with_correct_content(self, tmp_target):
        AtomicFileWriter.write(tmp_target, "hello world")
        assert os.path.exists(tmp_target)
        with open(tmp_target, "r", encoding="utf-8") as f:
            assert f.read() == "hello world"

    def test_write_replaces_existing_file_atomically(self, tmp_target):
        # 先写入旧内容
        with open(tmp_target, "w", encoding="utf-8") as f:
            f.write("old content")
        # 用原子写入覆盖
        AtomicFileWriter.write(tmp_target, "new content")
        with open(tmp_target, "r", encoding="utf-8") as f:
            assert f.read() == "new content"

    def test_write_preserves_unicode(self, tmp_target):
        text = "中文测试 🚀 émoji"
        AtomicFileWriter.write(tmp_target, text)
        with open(tmp_target, "r", encoding="utf-8") as f:
            assert f.read() == text

    def test_write_creates_parent_dirs(self, tmp_dir):
        target = os.path.join(tmp_dir, "a", "b", "c", "deep.txt")
        AtomicFileWriter.write(target, "deep content")
        assert os.path.exists(target)
        with open(target, "r", encoding="utf-8") as f:
            assert f.read() == "deep content"

    def test_write_empty_content(self, tmp_target):
        AtomicFileWriter.write(tmp_target, "")
        assert os.path.exists(tmp_target)
        assert os.path.getsize(tmp_target) == 0


class TestAtomicFileWriterAtomicity:
    """原子性保证测试。"""

    def test_write_uses_tmp_file_then_replace(self, tmp_target, tmp_dir):
        """验证写入流程：先写临时文件，再 os.replace 替换。"""
        with mock.patch("os.replace", wraps=os.replace) as mock_replace:
            AtomicFileWriter.write(tmp_target, "test content")
            # os.replace 应被调用
            assert mock_replace.called
        # 最终目标文件存在
        assert os.path.exists(tmp_target)

    def test_write_cleans_up_tmp_on_failure(self, tmp_target, tmp_dir):
        """写入失败时应清理临时文件。"""
        # 模拟 os.replace 抛出异常
        original_replace = os.replace

        def failing_replace(src, dst):
            raise OSError("simulated replace failure")

        with mock.patch("os.replace", side_effect=failing_replace):
            with pytest.raises(OSError):
                AtomicFileWriter.write(tmp_target, "test content")

        # 临时文件应被清理
        tmp_files = [
            f for f in os.listdir(tmp_dir)
            if f.startswith(".target.txt") and f.endswith(".tmp")
        ]
        assert len(tmp_files) == 0, f"残留临时文件: {tmp_files}"

    def test_write_does_not_corrupt_existing_on_failure(self, tmp_target):
        """写入失败时原文件应保持完整。"""
        # 先写入原始内容
        with open(tmp_target, "w", encoding="utf-8") as f:
            f.write("original")

        with mock.patch("os.replace", side_effect=OSError("fail")):
            with pytest.raises(OSError):
                AtomicFileWriter.write(tmp_target, "new content")

        # 原文件应保持不变
        with open(tmp_target, "r", encoding="utf-8") as f:
            assert f.read() == "original"

    def test_write_calls_fsync(self, tmp_target):
        """验证 fsync 被调用。"""
        with mock.patch("os.fsync", wraps=os.fsync) as mock_fsync:
            AtomicFileWriter.write(tmp_target, "test")
            assert mock_fsync.called


class TestAtomicFileWriterLargeFile:
    """大文件处理测试。"""

    def test_write_large_file_skips_atomic(self, tmp_target):
        """大文件（>10MB）应跳过原子写入直接写。"""
        # 构造 11MB 内容
        large_content = "x" * (11 * 1024 * 1024)
        with mock.patch("os.replace", wraps=os.replace) as mock_replace:
            AtomicFileWriter.write(tmp_target, large_content)
            # 大文件不应走 os.replace
            assert not mock_replace.called
        assert os.path.exists(tmp_target)
        assert os.path.getsize(tmp_target) == len(large_content)

    def test_write_below_threshold_uses_atomic(self, tmp_target):
        """低于 10MB 走原子写入。"""
        content = "x" * (1024 * 1024)  # 1MB
        with mock.patch("os.replace", wraps=os.replace) as mock_replace:
            AtomicFileWriter.write(tmp_target, content)
            assert mock_replace.called


class TestAtomicFileWriterAppendMode:
    """追加模式测试。"""

    def test_append_mode_bypasses_atomic(self, tmp_target):
        """追加模式直接写，不走原子替换。"""
        AtomicFileWriter.write(tmp_target, "first")
        with mock.patch("os.replace", wraps=os.replace) as mock_replace:
            AtomicFileWriter.write(tmp_target, " second", append=True)
            assert not mock_replace.called
        with open(tmp_target, "r", encoding="utf-8") as f:
            assert f.read() == "first second"

    def test_append_to_nonexistent_file(self, tmp_target):
        """追加到不存在的文件应创建。"""
        AtomicFileWriter.write(tmp_target, "appended", append=True)
        with open(tmp_target, "r", encoding="utf-8") as f:
            assert f.read() == "appended"

    def test_multiple_appends(self, tmp_target):
        AtomicFileWriter.write(tmp_target, "A", append=True)
        AtomicFileWriter.write(tmp_target, "B", append=True)
        AtomicFileWriter.write(tmp_target, "C", append=True)
        with open(tmp_target, "r", encoding="utf-8") as f:
            assert f.read() == "ABC"


class TestAtomicFileWriterEncoding:
    """编码参数测试。"""

    def test_write_with_utf8_explicit(self, tmp_target):
        AtomicFileWriter.write(tmp_target, "test", encoding="utf-8")
        with open(tmp_target, "r", encoding="utf-8") as f:
            assert f.read() == "test"

    def test_write_with_utf8_sig_encoding(self, tmp_target):
        """utf-8-sig 写入应在文件头加 BOM。"""
        AtomicFileWriter.write(tmp_target, "test", encoding="utf-8-sig")
        with open(tmp_target, "rb") as f:
            head = f.read(3)
            assert head == b"\xef\xbb\xbf"

    def test_write_with_gbk_encoding(self, tmp_target):
        """GBK 编码写入中文。"""
        AtomicFileWriter.write(tmp_target, "中文", encoding="gbk")
        with open(tmp_target, "rb") as f:
            data = f.read()
        # GBK 编码的 "中文"
        assert data == "中文".encode("gbk")


class TestAtomicFileWriterConcurrency:
    """并发写入测试。"""

    def test_concurrent_writes_to_different_files(self, tmp_dir):
        """并发写入不同文件应全部成功。"""
        results = []
        errors = []

        def worker(idx):
            try:
                target = os.path.join(tmp_dir, f"file_{idx}.txt")
                AtomicFileWriter.write(target, f"content-{idx}")
                results.append(idx)
            except Exception as e:
                errors.append((idx, e))

        threads = [threading.Thread(target=worker, args=(i,)) for i in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0, f"并发写入失败: {errors}"
        assert len(results) == 10
        for i in range(10):
            target = os.path.join(tmp_dir, f"file_{i}.txt")
            with open(target, "r", encoding="utf-8") as f:
                assert f.read() == f"content-{i}"

    def test_concurrent_writes_to_same_file_safe(self, tmp_target):
        """并发写入同一文件不应破坏文件结构。

        多个线程同时写同一文件，最终文件应为某次完整写入的内容，
        不应出现部分写入或文件损坏。

        注意：在 Windows 上，os.replace 对同一目标的并发调用可能抛出
        PermissionError，这是平台限制而非 bug。本测试的目标是验证
        "不损坏文件"，而非"全部成功"。Tier 1-6 ManagedPathLockManager
        实施后将解决并发写入失败问题。
        """
        # 多个线程写不同内容
        contents = [f"content-{i}" * 100 for i in range(5)]
        errors = []
        success_count = [0]
        lock = threading.Lock()

        def worker(content):
            try:
                AtomicFileWriter.write(tmp_target, content)
                with lock:
                    success_count[0] += 1
            except (OSError, PermissionError) as e:
                # Windows 上并发同路径 replace 可能抛 PermissionError
                with lock:
                    errors.append(e)

        threads = [threading.Thread(target=worker, args=(c,)) for c in contents]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # 至少一次写入成功
        assert success_count[0] >= 1, f"所有写入都失败: {errors}"
        # 文件应为某次完整写入的内容（不损坏）
        with open(tmp_target, "r", encoding="utf-8") as f:
            final = f.read()
        assert final in contents, f"文件内容损坏: {final[:50]!r}"


class TestAtomicFileWriterEdgeCases:
    """边界情况测试。"""

    def test_write_to_invalid_path_fails_gracefully(self):
        r"""写入无效路径应抛出异常而非崩溃。

        Windows 上绝对路径会被规范化（可写），改用真正无效的路径：
        - 含 null 字节（POSIX 拒绝）
        - 或不存在的盘符（Windows）
        """
        # null 字节在路径中在所有平台都非法
        with pytest.raises((OSError, ValueError, PermissionError, Exception)):
            AtomicFileWriter.write("invalid\x00path.txt", "test")

    def test_write_path_with_special_chars(self, tmp_dir):
        """路径含空格和 Unicode。"""
        target = os.path.join(tmp_dir, "文件 with space.txt")
        AtomicFileWriter.write(target, "content")
        assert os.path.exists(target)
        with open(target, "r", encoding="utf-8") as f:
            assert f.read() == "content"

    def test_constants_threshold_is_10mb(self):
        """验证大文件阈值是 10MB。"""
        assert AtomicFileWriter.LARGE_FILE_THRESHOLD == 10 * 1024 * 1024
