from __future__ import annotations

import importlib.util
from pathlib import Path

import pytest

from obsidian_sync.config import AppConfig, write_default_config
from obsidian_sync.crypto import EncryptionManager, encrypted_vault_path
from obsidian_sync.sync_engine import SyncEngine

HAS_CRYPTO = importlib.util.find_spec("cryptography") is not None


def _write(path: Path, text: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")


def _load_engine(cfg_path: Path) -> SyncEngine:
    cfg = AppConfig.load(cfg_path)
    return SyncEngine(cfg)


def test_rename_is_mirrored(tmp_path: Path) -> None:
    src = tmp_path / "src"
    tgt = tmp_path / "tgt"
    src.mkdir()
    tgt.mkdir()
    cfg_path = tmp_path / "obsidian-sync.toml"
    write_default_config(cfg_path, source_vault=src, synology_vault=tgt, windows_synology_vault="C:\\x", force=True)

    _write(src / "old.md", "hello\n")
    engine = _load_engine(cfg_path)
    try:
        engine.sync_once()
        (src / "old.md").rename(src / "new.md")
        stats = engine.sync_once()
        assert stats.renamed_to_target >= 1
        assert (tgt / "new.md").exists()
        assert not (tgt / "old.md").exists()
    finally:
        engine.close()


def test_manual_conflict_policy_records_conflict_without_overwrite(tmp_path: Path) -> None:
    src = tmp_path / "src"
    tgt = tmp_path / "tgt"
    src.mkdir()
    tgt.mkdir()
    cfg_path = tmp_path / "obsidian-sync.toml"
    write_default_config(cfg_path, source_vault=src, synology_vault=tgt, windows_synology_vault="C:\\x", force=True)

    content = cfg_path.read_text(encoding="utf-8")
    content = content.replace('conflict_policy = "latest"', 'conflict_policy = "manual"')
    cfg_path.write_text(content, encoding="utf-8")

    _write(src / "note.md", "base\n")

    engine = _load_engine(cfg_path)
    try:
        engine.sync_once()

        _write(src / "note.md", "from-source\n")
        _write(tgt / "note.md", "from-target\n")
        stats = engine.sync_once()

        assert stats.conflicts >= 1
        # Manual mode leaves both originals untouched.
        assert (src / "note.md").read_text(encoding="utf-8") == "from-source\n"
        assert (tgt / "note.md").read_text(encoding="utf-8") == "from-target\n"
        assert engine.list_conflicts(limit=10)
    finally:
        engine.close()


def test_rules_engine_filters_by_extension_and_tag(tmp_path: Path) -> None:
    src = tmp_path / "src"
    tgt = tmp_path / "tgt"
    src.mkdir()
    tgt.mkdir()
    cfg_path = tmp_path / "obsidian-sync.toml"
    write_default_config(cfg_path, source_vault=src, synology_vault=tgt, windows_synology_vault="C:\\x", force=True)

    content = cfg_path.read_text(encoding="utf-8")
    content = content.replace("rules_include_extensions = []", 'rules_include_extensions = [".md"]')
    content = content.replace("rules_include_tags = []", 'rules_include_tags = ["work"]')
    cfg_path.write_text(content, encoding="utf-8")

    _write(src / "work.md", "#work\nimportant\n")
    _write(src / "private.md", "#private\n")
    _write(src / "image.png", "not really png")

    engine = _load_engine(cfg_path)
    try:
        engine.sync_once()
        assert (tgt / "work.md").exists()
        assert not (tgt / "private.md").exists()
        assert not (tgt / "image.png").exists()
    finally:
        engine.close()


def test_retry_handles_transient_copy_error(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    src = tmp_path / "src"
    tgt = tmp_path / "tgt"
    src.mkdir()
    tgt.mkdir()
    cfg_path = tmp_path / "obsidian-sync.toml"
    write_default_config(cfg_path, source_vault=src, synology_vault=tgt, windows_synology_vault="C:\\x", force=True)

    content = cfg_path.read_text(encoding="utf-8")
    content = content.replace("io_retry_attempts = 5", "io_retry_attempts = 2")
    cfg_path.write_text(content, encoding="utf-8")

    _write(src / "note.md", "retry me")

    import obsidian_sync.sync_engine as sync_engine

    original = sync_engine.atomic_copy
    calls = {"n": 0}

    def flaky(src_path: Path, dst_path: Path) -> None:
        calls["n"] += 1
        if calls["n"] == 1:
            raise OSError("transient")
        return original(src_path, dst_path)

    monkeypatch.setattr(sync_engine, "atomic_copy", flaky)

    engine = _load_engine(cfg_path)
    try:
        stats = engine.sync_once()
        assert stats.errors == 0
        assert calls["n"] >= 2
        assert (tgt / "note.md").exists()
    finally:
        engine.close()


@pytest.mark.skipif(not HAS_CRYPTO, reason="cryptography is not installed")
def test_encrypted_deleted_snapshot_can_be_restored(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    src = tmp_path / "src"
    tgt = tmp_path / "tgt"
    src.mkdir()
    tgt.mkdir()
    cfg_path = tmp_path / "obsidian-sync.toml"
    write_default_config(
        cfg_path,
        source_vault=src,
        synology_vault=tgt,
        windows_synology_vault="C:\\x",
        force=True,
    )

    content = cfg_path.read_text(encoding="utf-8")
    content = content.replace("encryption_enabled = false", "encryption_enabled = true")
    content = content.replace(
        'encryption_passphrase_env = "OBSIDIAN_SYNC_PASSPHRASE"',
        'encryption_passphrase_env = "OSYNC_TEST_PASSPHRASE"',
    )
    cfg_path.write_text(content, encoding="utf-8")
    monkeypatch.setenv("OSYNC_TEST_PASSPHRASE", "unit-test-secret")

    _write(src / "secure.md", "very secret\n")

    engine = _load_engine(cfg_path)
    try:
        engine.sync_once()
        (src / "secure.md").unlink()
        engine.sync_once()

        deleted = engine.list_deleted_files(limit=10, include_restored=False)
        assert deleted

        snapshot = deleted[0]
        encrypted_path = Path(snapshot.snapshot_path)
        assert encrypted_path.exists()
        assert str(encrypted_path).endswith(".osync.enc")
        assert b"very secret" not in encrypted_path.read_bytes()

        engine.restore_snapshot(snapshot.snapshot_id, side="source", dry_run=False)
        assert (src / "secure.md").read_text(encoding="utf-8") == "very secret\n"
    finally:
        engine.close()


@pytest.mark.skipif(not HAS_CRYPTO, reason="cryptography is not installed")
def test_vault_encryption_syncs_encrypted_target_and_decrypts_back(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    src = tmp_path / "src"
    tgt = tmp_path / "tgt"
    src.mkdir()
    tgt.mkdir()
    cfg_path = tmp_path / "obsidian-sync.toml"
    write_default_config(
        cfg_path,
        source_vault=src,
        synology_vault=tgt,
        windows_synology_vault="C:\\x",
        force=True,
    )

    content = cfg_path.read_text(encoding="utf-8")
    content = content.replace("vault_encryption_enabled = false", "vault_encryption_enabled = true")
    content = content.replace(
        'encryption_passphrase_env = "OBSIDIAN_SYNC_PASSPHRASE"',
        'encryption_passphrase_env = "OSYNC_TEST_PASSPHRASE"',
    )
    cfg_path.write_text(content, encoding="utf-8")
    monkeypatch.setenv("OSYNC_TEST_PASSPHRASE", "unit-test-secret")

    note_rel = "note.md"
    note_src = src / note_rel
    note_enc = encrypted_vault_path(tgt / note_rel)
    _write(note_src, "from-source\n")

    engine = _load_engine(cfg_path)
    try:
        engine.sync_once()
        assert not (tgt / note_rel).exists()
        assert note_enc.exists()
        assert b"from-source" not in note_enc.read_bytes()

        manager = EncryptionManager(passphrase="unit-test-secret")
        encrypted_remote = manager.encrypt_bytes(b"from-cloud\n")
        note_enc.write_bytes(encrypted_remote)

        engine.sync_once()
        assert note_src.read_text(encoding="utf-8") == "from-cloud\n"
    finally:
        engine.close()


@pytest.mark.skipif(not HAS_CRYPTO, reason="cryptography is not installed")
def test_enable_vault_encryption_migrates_existing_plain_target(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    src = tmp_path / "src"
    tgt = tmp_path / "tgt"
    src.mkdir()
    tgt.mkdir()
    cfg_path = tmp_path / "obsidian-sync.toml"
    write_default_config(
        cfg_path,
        source_vault=src,
        synology_vault=tgt,
        windows_synology_vault="C:\\x",
        force=True,
    )

    content = cfg_path.read_text(encoding="utf-8")
    content = content.replace(
        'encryption_passphrase_env = "OBSIDIAN_SYNC_PASSPHRASE"',
        'encryption_passphrase_env = "OSYNC_TEST_PASSPHRASE"',
    )
    cfg_path.write_text(content, encoding="utf-8")
    monkeypatch.setenv("OSYNC_TEST_PASSPHRASE", "unit-test-secret")

    note_rel = "migrate.md"
    note_src = src / note_rel
    note_plain_target = tgt / note_rel
    note_enc_target = encrypted_vault_path(note_plain_target)

    _write(note_src, "before-migrate\n")

    engine = _load_engine(cfg_path)
    try:
        engine.sync_once()
    finally:
        engine.close()

    assert note_plain_target.exists()

    migrated = cfg_path.read_text(encoding="utf-8").replace(
        "vault_encryption_enabled = false",
        "vault_encryption_enabled = true",
    )
    cfg_path.write_text(migrated, encoding="utf-8")

    engine = _load_engine(cfg_path)
    try:
        engine.sync_once()
    finally:
        engine.close()

    assert note_src.exists()
    assert note_src.read_text(encoding="utf-8") == "before-migrate\n"
    assert note_enc_target.exists()
    assert not note_plain_target.exists()
