"""
Tests for file_lock utility and concurrent access protection.
Uses subprocess-based workers to simulate real multi-process concurrency.
"""
import sys
import tempfile
import subprocess
import time
import json
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent))

import pytest
from aacode.utils.file_lock import file_lock


class TestFileLock:
    """Test the file_lock context manager itself."""

    def test_basic_lock(self):
        with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
            f.write("initial")
            fname = f.name

        fpath = Path(fname)
        try:
            with file_lock(fpath):
                fpath.write_text("updated")
            assert fpath.read_text() == "updated"
        finally:
            fpath.unlink(missing_ok=True)

    def test_lock_file_created(self):
        with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
            fname = f.name

        fpath = Path(fname)
        lockfile = fpath.with_suffix(fpath.suffix + ".lock")
        try:
            assert not lockfile.exists()
            with file_lock(fpath):
                assert lockfile.exists()
        finally:
            fpath.unlink(missing_ok=True)
            lockfile.unlink(missing_ok=True)

    def test_different_files_independent(self):
        with (
            tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f1,
            tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f2,
        ):
            p1 = Path(f1.name)
            p2 = Path(f2.name)

        try:
            with file_lock(p1):
                p1.write_text("a")
                with file_lock(p2):
                    p2.write_text("b")
            assert p1.read_text() == "a"
            assert p2.read_text() == "b"
        finally:
            p1.unlink(missing_ok=True)
            p2.unlink(missing_ok=True)

    def test_lock_released_after_context(self):
        import fcntl

        with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
            fname = f.name

        fpath = Path(fname)
        try:
            with file_lock(fpath):
                fpath.write_text("hello")
            lockfile_str = str(fpath) + ".lock"
            with open(lockfile_str, "a") as lf:
                try:
                    fcntl.flock(lf.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
                    fcntl.flock(lf.fileno(), fcntl.LOCK_UN)
                except BlockingIOError:
                    pytest.fail("Lock should have been released but was still held")
        finally:
            fpath.unlink(missing_ok=True)


class TestLockedCounter:
    """Verify locks prevent classic read-modify-write data loss."""

    def test_locked_counter_no_loss(self):
        """Multi-process concurrent increments with lock: all counts survive."""
        tmpdir = Path(tempfile.mkdtemp())
        counter_file = tmpdir / "counter.txt"
        counter_file.write_text("0")

        num_workers = 20
        script = tmpdir / "worker.py"
        script.write_text(f"""
import sys
sys.path.insert(0, {repr(str(Path(__file__).parent.parent))})
from aacode.utils.file_lock import file_lock
from pathlib import Path

counter_file = Path({repr(str(counter_file))})
with file_lock(counter_file):
    val = int(counter_file.read_text().strip())
    val += 1
    counter_file.write_text(str(val))
""")

        processes = []
        for _ in range(num_workers):
            p = subprocess.Popen(
                [sys.executable, str(script)],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            processes.append(p)

        for p in processes:
            out, err = p.communicate(timeout=30)
            assert p.returncode == 0, f"Worker failed: {err.decode()}"

        final_val = int(counter_file.read_text().strip())
        assert final_val == num_workers, (
            f"With lock: expected {num_workers}, got {final_val}"
        )

    def test_unlocked_loses_data(self):
        """Multi-process concurrent increments without lock: data is lost."""
        tmpdir = Path(tempfile.mkdtemp())
        counter_file = tmpdir / "counter_unlocked.txt"
        counter_file.write_text("0")

        num_workers = 40
        script = tmpdir / "worker_unlocked.py"
        script.write_text(f"""
from pathlib import Path
import time
import random

counter_file = Path({repr(str(counter_file))})
time.sleep(0.02 + random.random() * 0.05)
val = int(counter_file.read_text().strip())
val += 1
counter_file.write_text(str(val))
""")

        processes = []
        for _ in range(num_workers):
            p = subprocess.Popen(
                [sys.executable, str(script)],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            processes.append(p)

        for p in processes:
            p.communicate(timeout=30)

        final_val = int(counter_file.read_text().strip())
        assert final_val <= num_workers, (
            f"Without lock: expected <= {num_workers}, got {final_val}"
        )


class TestTodoManagerConcurrency:
    """Test concurrent todo operations via subprocess workers."""

    @pytest.fixture(autouse=True)
    def setup(self):
        self.tmpdir = Path(tempfile.mkdtemp())

    def _write_worker_script(self, name, code):
        script = self.tmpdir / f"{name}.py"
        script.write_text(code)
        return script

    def test_concurrent_add_todo_items(self):
        """Multi-process concurrent add_todo_item: no data loss."""
        project_path = self.tmpdir / "project"
        project_path.mkdir()
        parent_dir = Path(__file__).parent.parent

        setup_script = self._write_worker_script("todo_setup", f"""
import sys, asyncio
sys.path.insert(0, {repr(str(parent_dir))})
from pathlib import Path
from aacode.utils.todo_manager import TodoManager

async def main():
    mgr = TodoManager(Path({repr(str(project_path))}))
    await mgr.create_todo_list("Concurrent add test", context_manager=None)

asyncio.run(main())
""")
        subprocess.run([sys.executable, str(setup_script)], check=True, capture_output=True, timeout=30)

        todo_files = sorted(list((project_path / ".aacode" / "todos").glob("*.md")),
                           key=lambda p: p.stat().st_mtime)
        assert len(todo_files) == 1, f"Expected 1 todo file, got {len(todo_files)}"
        todo_file_path = todo_files[-1]

        script = self._write_worker_script("todo_worker", f"""
import sys, asyncio
sys.path.insert(0, {repr(str(parent_dir))})
from pathlib import Path
from aacode.utils.todo_manager import TodoManager

async def main(worker_id):
    mgr = TodoManager(Path({repr(str(project_path))}))
    mgr.current_todo_file = Path({repr(str(todo_file_path))})
    for j in range(10):
        await mgr.add_todo_item(f"Worker-{{worker_id}}-{{j}}", priority="medium")
    print(f"WORKER_{{worker_id}}_DONE")

asyncio.run(main({repr('SENTINEL_REPLACE')}))
""")

        num_workers = 5
        processes = []
        for i in range(num_workers):
            worker_script = self._write_worker_script(f"todo_worker_{i}",
                script.read_text().replace(f"{repr('SENTINEL_REPLACE')}", str(i)))
            p = subprocess.Popen(
                [sys.executable, str(worker_script)],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            processes.append(p)

        for p in processes:
            out, err = p.communicate(timeout=60)
            assert p.returncode == 0, f"Worker failed: {err.decode()}"
            assert b"DONE" in out, f"Worker did not complete: {out.decode()}"

        content = todo_file_path.read_text()
        item_count = content.count("- [ ]")
        expected = num_workers * 10
        assert item_count == expected, (
            f"Expected {expected} items, found {item_count}.\nContent first 500 chars:\n{content[:500]}"
        )

    def test_concurrent_mark_completed(self):
        """Multi-process concurrent mark_todo_completed: no data loss."""
        project_path = self.tmpdir / "project_mark"
        project_path.mkdir()
        parent_dir = Path(__file__).parent.parent

        setup_script = self._write_worker_script("mark_setup", f"""
import sys, asyncio
sys.path.insert(0, {repr(str(parent_dir))})
from pathlib import Path
from aacode.utils.todo_manager import TodoManager

async def main():
    mgr = TodoManager(Path({repr(str(project_path))}))
    await mgr.create_todo_list("Mark test", context_manager=None)
    for j in range(20):
        await mgr.add_todo_item(f"Task {{j}}", priority="medium")
    mgr.current_todo_file = mgr.todo_dir / sorted(mgr.todo_dir.glob("*.md"), key=lambda p: p.stat().st_mtime)[-1].name

asyncio.run(main())
""")
        subprocess.run([sys.executable, str(setup_script)], check=True, capture_output=True, timeout=30)

        todo_files = sorted(list((project_path / ".aacode" / "todos").glob("*.md")), key=lambda p: p.stat().st_mtime)
        assert len(todo_files) > 0
        todo_file_path = todo_files[-1]

        mark_script = self._write_worker_script("mark_worker", f"""
import sys, asyncio
sys.path.insert(0, {repr(str(parent_dir))})
from pathlib import Path
from aacode.utils.todo_manager import TodoManager

async def main(worker_id):
    mgr = TodoManager(Path({repr(str(project_path))}))
    mgr.current_todo_file = Path({repr(str(todo_file_path))})
    for _ in range(4):
        success = await mgr.mark_todo_completed(item_pattern="Task")
        if not success:
            break
    print(f"MARK_WORKER_{{worker_id}}_DONE")

asyncio.run(main({repr('SENTINEL_REPLACE')}))
""")

        num_workers = 5
        processes = []
        for i in range(num_workers):
            worker_script = self._write_worker_script(f"mark_worker_{i}",
                mark_script.read_text().replace(f"{repr('SENTINEL_REPLACE')}", str(i)))
            p = subprocess.Popen(
                [sys.executable, str(worker_script)],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            processes.append(p)

        for p in processes:
            out, err = p.communicate(timeout=60)
            assert b"DONE" in out, f"Worker failed: {out.decode()}, {err.decode()}"

        content = todo_file_path.read_text()
        completed = content.count("- [x]")
        assert completed > 0, "At least some items should be marked completed"
        total = content.count("- [ ]") + content.count("- [x]")
        assert total == 20, f"Expected 20 items total, found {total}"


class TestContextManagerConcurrency:
    """Test concurrent context manager updates via subprocess workers."""

    def test_concurrent_observation_history(self):
        tmpdir = Path(tempfile.mkdtemp())
        project_path = tmpdir / "ctx_project"
        project_path.mkdir()
        parent_dir = Path(__file__).parent.parent

        script = tmpdir / "ctx_worker.py"
        script.write_text(f"""
import sys, asyncio
sys.path.insert(0, {repr(str(parent_dir))})
from pathlib import Path
from aacode.utils.context_manager import ContextManager

async def main(worker_id):
    mgr = ContextManager(Path({repr(str(project_path))}))
    for j in range(5):
        await mgr.update(f"Worker-{{worker_id}} obs {{j}}: testing concurrent context updates")
    print(f"CTX_WORKER_{{worker_id}}_DONE")

asyncio.run(main({repr('SENTINEL')}))
""")

        num_workers = 5
        processes = []
        for i in range(num_workers):
            worker_script = tmpdir / f"ctx_worker_{i}.py"
            worker_script.write_text(script.read_text().replace(repr("SENTINEL"), str(i)))
            p = subprocess.Popen(
                [sys.executable, str(worker_script)],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            processes.append(p)

        for p in processes:
            out, err = p.communicate(timeout=60)
            assert b"DONE" in out, f"Worker failed: {err.decode()}"

        history_file = project_path / ".aacode" / "context" / "observation_history.txt"
        assert history_file.exists(), "History file should exist"
        content = history_file.read_text()
        entries = content.strip().split("\n---\n")
        assert 1 <= len(entries) <= 5, (
            f"Expected 1-5 entries, got {len(entries)}"
        )


class TestRegression:
    """Ensure existing functionality is not broken."""

    def test_session_manager_import_and_create(self):
        """SessionManager can be imported and basic operations work."""
        from aacode.utils.session_manager import SessionManager, SessionMessage

        tmpdir = Path(tempfile.mkdtemp())
        sm = SessionManager(tmpdir)
        assert sm.project_path == tmpdir
        assert sm.sessions_dir.exists()

    def test_todo_manager_basic_operations(self):
        """TodoManager basic create/add/complete works."""
        import asyncio
        from aacode.utils.todo_manager import TodoManager

        async def test():
            tmpdir = Path(tempfile.mkdtemp())
            mgr = TodoManager(tmpdir)
            await mgr.create_todo_list("Test regression")
            tid = await mgr.add_todo_item("Test item", priority="high")
            assert tid is not None
            assert tid.startswith("t")
            content = mgr.current_todo_file.read_text()
            assert "Test item" in content
            ok = await mgr.mark_todo_completed(todo_id=tid)
            assert ok
            content = mgr.current_todo_file.read_text()
            assert "- [x]" in content
            return True

        result = asyncio.run(test())
        assert result

    def test_context_manager_basic_operations(self):
        """ContextManager basic update works."""
        import asyncio
        from aacode.utils.context_manager import ContextManager

        async def test():
            tmpdir = Path(tempfile.mkdtemp())
            mgr = ContextManager(tmpdir)
            await mgr.update("Test observation")
            latest = mgr.context_dir / "latest_observation.txt"
            assert latest.exists()
            assert "Test observation" in latest.read_text()
            return True

        result = asyncio.run(test())
        assert result


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