import json

from vcm_file_uploader.watchman import BackupWatchman


def _populate_dir(d, files: dict[str, bytes | str]):
    """Create files in directory d from a dict of {name: content}."""
    for name, content in files.items():
        p = d / name
        p.parent.mkdir(parents=True, exist_ok=True)
        if isinstance(content, str):
            p.write_text(content)
        else:
            p.write_bytes(content)


class TestBackupWatchmanScan:
    def test_first_scan_all_new(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "output"
        watch.mkdir()
        _populate_dir(watch, {
            "a.txt": "hello",
            "subdir/b.txt": "world",
        })

        wm = BackupWatchman(watch, output)
        plan = wm.scan()

        assert plan["summary"]["upload_count"] == 2
        assert plan["summary"]["upload_bytes"] > 0
        assert plan["summary"]["deletion_count"] == 0
        assert len(plan["files"]) == 2
        assert "timestamp" in plan

        # All files should have reason "new"
        for f in plan["files"]:
            assert f["reason"] == "new"

    def test_second_scan_no_changes(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "output"
        watch.mkdir()
        _populate_dir(watch, {"a.txt": "hello"})

        wm = BackupWatchman(watch, output)
        wm.scan()  # first scan
        plan = wm.scan()  # second scan, no changes

        assert plan["summary"]["upload_count"] == 0
        assert plan["summary"]["deletion_count"] == 0

    def test_detects_new_file(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "output"
        watch.mkdir()
        _populate_dir(watch, {"a.txt": "hello"})

        wm = BackupWatchman(watch, output)
        wm.scan()

        # Add a new file
        _populate_dir(watch, {"b.txt": "new file"})
        plan = wm.scan()

        assert plan["summary"]["upload_count"] == 1
        assert plan["files"][0]["path"] == "b.txt"

    def test_detects_modified_file(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "output"
        watch.mkdir()
        _populate_dir(watch, {"a.txt": "original"})

        wm = BackupWatchman(watch, output)
        wm.scan()

        # Modify the file
        (watch / "a.txt").write_text("modified content that is different")
        plan = wm.scan()

        assert plan["summary"]["upload_count"] == 1
        assert plan["files"][0]["path"] == "a.txt"

    def test_detects_deletion(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "output"
        watch.mkdir()
        _populate_dir(watch, {"a.txt": "hello", "b.txt": "world"})

        wm = BackupWatchman(watch, output)
        wm.scan()

        # Delete a file
        (watch / "b.txt").unlink()
        plan = wm.scan()

        assert plan["summary"]["deletion_count"] == 1
        assert "b.txt" in plan["deletions"]


class TestBackupWatchmanPlanFormat:
    def test_files_sorted_largest_first(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "output"
        watch.mkdir()
        _populate_dir(watch, {
            "small.txt": "x",
            "big.txt": "x" * 1000,
            "medium.txt": "x" * 100,
        })

        wm = BackupWatchman(watch, output)
        plan = wm.scan()

        sizes = [f["size"] for f in plan["files"]]
        assert sizes == sorted(sizes, reverse=True)

    def test_file_entry_fields(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "output"
        watch.mkdir()
        _populate_dir(watch, {"data.txt": "content"})

        wm = BackupWatchman(watch, output)
        plan = wm.scan()

        entry = plan["files"][0]
        assert "path" in entry
        assert "full_path" in entry
        assert "size" in entry
        assert "sha256" in entry
        assert "category" in entry
        assert "reason" in entry
        assert "multipart" in entry

    def test_multipart_flag(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "output"
        watch.mkdir()
        _populate_dir(watch, {"big.bin": b"x" * 100})

        wm = BackupWatchman(watch, output, multipart_threshold=50)
        plan = wm.scan()

        assert plan["files"][0]["multipart"] is True
        assert plan["summary"]["multipart_count"] == 1

    def test_plan_written_to_disk(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "output"
        watch.mkdir()
        _populate_dir(watch, {"a.txt": "hello"})

        wm = BackupWatchman(watch, output)
        wm.scan()

        plan_files = list(output.glob("upload_*.json"))
        assert len(plan_files) == 1

        with open(plan_files[0]) as f:
            plan = json.load(f)
        assert plan["summary"]["upload_count"] == 1

    def test_manifest_rotation(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "output"
        watch.mkdir()
        _populate_dir(watch, {"a.txt": "hello"})

        wm = BackupWatchman(watch, output)
        wm.scan()

        assert (output / "manifest_prev.json").exists()
        assert not (output / "manifest_new.json").exists()


class TestBackupWatchmanConfig:
    def test_creates_output_dir(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "deep" / "nested" / "output"
        watch.mkdir()
        _populate_dir(watch, {"a.txt": "hello"})

        wm = BackupWatchman(watch, output)
        wm.scan()

        assert output.exists()

    def test_empty_directory(self, tmp_path):
        watch = tmp_path / "work"
        output = tmp_path / "output"
        watch.mkdir()

        wm = BackupWatchman(watch, output)
        plan = wm.scan()

        assert plan["summary"]["upload_count"] == 0
