# tests/test_backup.py

import pytest
from unittest.mock import patch, MagicMock
import os
import json
from geminiai_cli import backup
from geminiai_cli.config import DEFAULT_GEMINI_HOME

# Note: We rely on pyfakefs (fs fixture) which is autouse in conftest.py
# So standard os operations work on the fake filesystem.

@patch("geminiai_cli.backup.fcntl")
def test_acquire_lock_success(mock_fcntl, fs):
    fs.create_file('/tmp/gemini.lock')
    # Patch backup.LOCKFILE to point to our fake file
    with patch("geminiai_cli.backup.LOCKFILE", "/tmp/gemini.lock"):
        fd = backup.acquire_lock("/tmp/gemini.lock")
        assert fd is not None
        mock_fcntl.flock.assert_called()

@patch("geminiai_cli.backup.fcntl")
def test_acquire_lock_fail(mock_fcntl, fs):
    fs.create_file('/tmp/gemini.lock')
    mock_fcntl.flock.side_effect = BlockingIOError
    with pytest.raises(SystemExit) as e:
        backup.acquire_lock("/tmp/gemini.lock")
    assert e.value.code == 2

def test_run():
    with patch("subprocess.run") as mock_run:
        backup.run("ls")
        mock_run.assert_called_with("ls", shell=True, check=True)

def test_run_capture():
    with patch("subprocess.run") as mock_run:
        backup.run("ls", capture=True)
        # subprocess.run(..., stdout=PIPE, stderr=PIPE)
        # Check call args more loosely or strictly
        kwargs = mock_run.call_args[1]
        assert kwargs.get("stdout") is not None
        assert kwargs.get("stderr") is not None

def test_read_active_email_no_file(fs):
    assert backup.read_active_email("/tmp") is None

def test_read_active_email_valid(fs):
    data = json.dumps({"active": "user@example.com"})
    fs.create_file("/tmp/google_accounts.json", contents=data)
    assert backup.read_active_email("/tmp") == "user@example.com"

def test_read_active_email_invalid_json(fs):
    fs.create_file("/tmp/google_accounts.json", contents="{invalid")
    assert backup.read_active_email("/tmp") is None

def test_read_active_email_no_active_field(fs):
    fs.create_file("/tmp/google_accounts.json", contents="{}")
    assert backup.read_active_email("/tmp") is None

def test_ensure_dir(fs):
    backup.ensure_dir("/tmp/dir")
    assert os.path.exists("/tmp/dir")

def test_make_timestamp():
    assert len(backup.make_timestamp()) > 0

def test_atomic_symlink(fs):
    fs.create_file("target")
    backup.atomic_symlink("target", "link")
    assert os.path.islink("link")
    assert os.readlink("link") == "target"

def test_atomic_symlink_exceptions(fs):
    fs.create_file("target")
    with patch("os.symlink", side_effect=OSError("Symlink fail")):
        with pytest.raises(OSError):
            backup.atomic_symlink("target", "link")

@patch("geminiai_cli.backup.acquire_lock")
@patch("geminiai_cli.backup.read_active_email", return_value="user@example.com")
@patch("geminiai_cli.backup.run")
@patch("os.replace") # Mock os.replace to bypass pyfakefs limitation for symlink rename
def test_main_success(mock_replace, mock_run, mock_email, mock_lock, fs):
    # Setup source directory
    fs.create_dir(DEFAULT_GEMINI_HOME)
    fs.create_file(os.path.join(DEFAULT_GEMINI_HOME, "file"))

    mock_run.return_value.returncode = 0

    # We also need to patch os.path.lexists because atomic_symlink uses it and mocking os.replace might interfere
    # But wait, the failure was os.replace(tmp_dest, dest).
    # This is backup step 4 (directory backup).
    # tmp_dest is copied via cp -a. We mocked run('cp -a ...').
    # But run is mocked, so the CP command never ran!
    # So tmp_dest DOES NOT EXIST in the fake fs.

    # SOLUTION: We must manually create tmp_dest in the test since we mocked cp.
    # OR unmock 'run' but 'run' calls subprocess which we shouldn't use.
    # So we simulate the effect of 'cp -a' by creating the directory.

    # We need to know the timestamp to predict the tmp name.
    with patch("geminiai_cli.backup.make_timestamp", return_value="2025-01-01_120000"):
        # tmp_dest = ... + ".tmp-..."
        # logic: tmp_dest = os.path.join(tmp_parent, f".{os.path.basename(dest)}.tmp-{ts}")
        # We need to create this directory in fs before main calls os.replace.

        # But we don't know the exact paths main will derive easily without duplicating logic.
        # But we can patch shutil.rmtree and os.replace to do nothing, or verify calls.

        # If we patch os.replace, the FileNotFoundError won't happen.
        # And we can verify os.replace was called.

        with patch("sys.argv", ["backup.py"]):
            backup.main()

    assert mock_replace.call_count >= 1 # One for directory move, maybe one for symlink

@patch("geminiai_cli.backup.acquire_lock")
@patch("geminiai_cli.backup.read_active_email", return_value="user@example.com")
@patch("geminiai_cli.backup.run")
def test_main_diff_fail(mock_run, mock_email, mock_lock, fs):
    fs.create_dir(DEFAULT_GEMINI_HOME)

    with patch("sys.argv", ["backup.py"]):
        mock_run.return_value.returncode = 1
        with pytest.raises(SystemExit) as e:
            backup.main()
        assert e.value.code == 3

@patch("geminiai_cli.backup.acquire_lock")
@patch("geminiai_cli.backup.read_active_email", return_value="user@example.com")
def test_main_src_not_exist(mock_email, mock_lock, fs):
    # DEFAULT_GEMINI_HOME not created
    with patch("sys.argv", ["backup.py"]):
        with pytest.raises(SystemExit) as e:
            backup.main()
        assert e.value.code == 1

@patch("geminiai_cli.backup.acquire_lock")
@patch("geminiai_cli.backup.read_active_email", return_value="user@example.com")
@patch("geminiai_cli.backup.run")
def test_main_dry_run(mock_run, mock_email, mock_lock, fs):
    fs.create_dir(DEFAULT_GEMINI_HOME)
    with patch("sys.argv", ["backup.py", "--dry-run"]):
        backup.main()
        mock_run.assert_not_called()

@patch("geminiai_cli.backup.acquire_lock")
@patch("geminiai_cli.backup.read_active_email", return_value="user@example.com")
@patch("geminiai_cli.backup.run")
@patch("geminiai_cli.backup.get_cloud_provider")
@patch("os.replace")
def test_main_cloud(mock_replace, mock_get_provider, mock_run, mock_email, mock_lock, fs):
    fs.create_dir(DEFAULT_GEMINI_HOME)

    with patch("sys.argv", ["backup.py", "--cloud", "--bucket", "b", "--b2-id", "i", "--b2-key", "k"]):
        mock_run.return_value.returncode = 0
        mock_b2 = MagicMock()
        mock_get_provider.return_value = mock_b2

        backup.main()

        mock_get_provider.assert_called()
        mock_b2.upload_file.assert_called()

@patch("geminiai_cli.backup.acquire_lock")
@patch("geminiai_cli.backup.read_active_email", return_value="user@example.com")
@patch("geminiai_cli.backup.run")
@patch("geminiai_cli.backup.get_cloud_provider", return_value=None)
@patch("geminiai_cli.credentials.get_setting", return_value=None)
@patch("os.replace")
@patch.dict(os.environ, {}, clear=True)
def test_main_cloud_missing_creds(mock_replace, mock_get_setting, mock_get_provider, mock_run, mock_email, mock_lock, fs):
    fs.create_dir(DEFAULT_GEMINI_HOME)

    with patch("sys.argv", ["backup.py", "--cloud"]):
        mock_run.return_value.returncode = 0
        with pytest.raises(SystemExit) as e:
            backup.main()
        assert e.value.code == 1

@patch("geminiai_cli.backup.acquire_lock")
@patch("geminiai_cli.backup.read_active_email", return_value=None)
@patch("geminiai_cli.backup.run")
@patch("os.replace")
def test_main_no_active_email(mock_replace, mock_run, mock_email, mock_lock, fs):
    fs.create_dir(DEFAULT_GEMINI_HOME)
    with patch("sys.argv", ["backup.py"]):
        mock_run.return_value.returncode = 0
        backup.main()
        assert mock_run.call_count >= 2

@patch("geminiai_cli.backup.acquire_lock")
@patch("geminiai_cli.backup.read_active_email", return_value="user@example.com")
@patch("geminiai_cli.backup.run")
@patch("geminiai_cli.backup.atomic_symlink", side_effect=Exception("Symlink error"))
@patch("os.replace")
def test_main_symlink_fail(mock_replace, mock_symlink, mock_run, mock_email, mock_lock, fs):
    fs.create_dir(DEFAULT_GEMINI_HOME)
    with patch("sys.argv", ["backup.py"]):
        mock_run.return_value.returncode = 0
        backup.main()
        mock_symlink.assert_called()

@patch("geminiai_cli.backup.acquire_lock")
@patch("geminiai_cli.backup.read_active_email", return_value="user@example.com")
@patch("geminiai_cli.backup.run")
@patch("os.replace")
def test_main_tmp_exists(mock_replace, mock_run, mock_email, mock_lock, fs):
    fs.create_dir(DEFAULT_GEMINI_HOME)
    mock_run.return_value.returncode = 0
    with patch("sys.argv", ["backup.py"]):
        backup.main()

@patch("geminiai_cli.backup.acquire_lock")
@patch("os.replace")
def test_main_lock_exception(mock_replace, mock_lock, fs):
    fs.create_dir(DEFAULT_GEMINI_HOME)
    mock_fd = MagicMock()
    mock_lock.return_value = mock_fd

    with patch("geminiai_cli.backup.read_active_email", return_value="user@example.com"):
        with patch("geminiai_cli.backup.run") as mock_run:
             mock_run.return_value.returncode = 0
             with patch("sys.argv", ["backup.py"]):
                 with patch("geminiai_cli.backup.fcntl.flock") as mock_flock:
                     mock_flock.side_effect = [None, Exception("Unlock fail")]
                     # Should not crash
                     backup.main()

@patch("geminiai_cli.backup.acquire_lock")
@patch("geminiai_cli.backup.read_active_email", return_value="user@example.com")
@patch("geminiai_cli.backup.run")
def test_main_diff_fail_no_stdout(mock_run, mock_email, mock_lock, fs):
    fs.create_dir(DEFAULT_GEMINI_HOME)
    with patch("sys.argv", ["backup.py"]):
        mock_run.return_value.returncode = 2
        mock_run.return_value.stdout = ""
        with pytest.raises(SystemExit) as e:
            backup.main()
        assert e.value.code == 3
