import json
import logging
import pytest
from unittest.mock import patch, MagicMock
import subprocess
from lastpass_vault import LastPassVault, LastPassError

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def vault():
    return LastPassVault()


# ===========================================================================
# _run_command
# ===========================================================================

def test_run_command_success(vault):
    with patch("subprocess.run") as mock_run:
        mock_run.return_value = MagicMock(
            stdout="success_output\n",
            stderr="",
            returncode=0
        )
        result = vault._run_command(["status"])
        assert result == "success_output"
        mock_run.assert_called_once_with(
            ["lpass", "status"],
            input=None,
            capture_output=True,
            text=True,
            check=True
        )


def test_run_command_failure(vault):
    with patch("subprocess.run") as mock_run:
        mock_run.side_effect = subprocess.CalledProcessError(
            returncode=1,
            cmd=["lpass", "status"],
            stderr="Error message"
        )
        with pytest.raises(LastPassError, match="Command 'lpass status' failed: Error message"):
            vault._run_command(["status"])


def test_run_command_no_shell(vault):
    """_run_command must never pass shell=True to subprocess.run."""
    with patch("subprocess.run") as mock_run:
        mock_run.return_value = MagicMock(stdout="ok\n", stderr="", returncode=0)
        vault._run_command(["status"])
        _, kwargs = mock_run.call_args
        assert kwargs.get("shell", False) is False


def test_run_command_passes_input_data_via_stdin(vault):
    """Sensitive data must reach the subprocess via the input kwarg, not argv."""
    with patch("subprocess.run") as mock_run:
        mock_run.return_value = MagicMock(stdout="ok\n", stderr="", returncode=0)
        vault._run_command(["login", "user@example.com"], input_data="s3cret")
        _, kwargs = mock_run.call_args
        assert kwargs["input"] == "s3cret"


# ===========================================================================
# _validate_sync
# ===========================================================================

def test_validate_sync_valid_values(vault):
    for value in ("auto", "now", "no"):
        vault._validate_sync(value)  # Must not raise.


def test_validate_sync_none_is_allowed(vault):
    vault._validate_sync(None)  # Must not raise.


def test_validate_sync_invalid_raises(vault):
    with pytest.raises(ValueError, match="sync must be one of"):
        vault._validate_sync("always")


# ===========================================================================
# login
# ===========================================================================

def test_login_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "Success"
        assert vault.login("user@example.com", "password123") is True
        mock_run_cmd.assert_called_once_with(
            ["login", "user@example.com"], input_data="password123"
        )


def test_login_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Login failed")
        assert vault.login("user@example.com", "wrong_pass") is False


def test_login_with_trust(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "Success"
        vault.login("user@example.com", trust=True)
        args, _ = mock_run_cmd.call_args
        assert "--trust" in args[0]
        assert "user@example.com" in args[0]


def test_login_with_plaintext_key_emits_security_warning(vault, caplog):
    with patch.object(LastPassVault, "_run_command", return_value="ok"):
        with caplog.at_level(logging.WARNING):
            vault.login("user@example.com", plaintext_key=True)
    assert any("plaintext" in rec.message.lower() for rec in caplog.records), (
        "Expected a security warning about plaintext-key storage"
    )


def test_login_with_plaintext_key_appends_flag(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "ok"
        vault.login("user@example.com", plaintext_key=True)
        args, _ = mock_run_cmd.call_args
        assert "--plaintext-key" in args[0]


def test_login_with_force(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "ok"
        vault.login("user@example.com", plaintext_key=True, force=True)
        args, _ = mock_run_cmd.call_args
        assert "--force" in args[0]


def test_login_password_not_in_argv(vault):
    """Master password must be in input_data, never in the command args."""
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "ok"
        vault.login("user@example.com", "supersecret")
        args, kwargs = mock_run_cmd.call_args
        assert "supersecret" not in args[0], "Password leaked into argv"
        assert kwargs.get("input_data") == "supersecret"


# ===========================================================================
# logout
# ===========================================================================

def test_logout_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        assert vault.logout() is True
        mock_run_cmd.assert_called_once_with(["logout"])


def test_logout_force(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        assert vault.logout(force=True) is True
        mock_run_cmd.assert_called_once_with(["logout", "--force"])


# ===========================================================================
# passwd
# ===========================================================================

def test_passwd_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        assert vault.passwd() is True
        mock_run_cmd.assert_called_once_with(["passwd"])


def test_passwd_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not logged in")
        assert vault.passwd() is False


# ===========================================================================
# is_logged_in
# ===========================================================================

def test_is_logged_in_true(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "Logged in as user@example.com."
        assert vault.is_logged_in() is True


def test_is_logged_in_false(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not logged in.")
        assert vault.is_logged_in() is False


# ===========================================================================
# sync
# ===========================================================================

def test_sync_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        assert vault.sync() is True
        mock_run_cmd.assert_called_once_with(["sync"])


def test_sync_with_background(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.sync(background=True)
        mock_run_cmd.assert_called_once_with(["sync", "--background"])


def test_sync_with_sync_type(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.sync(sync_type="now")
        mock_run_cmd.assert_called_once_with(["sync", "--sync=now"])


def test_sync_invalid_sync_type(vault):
    with pytest.raises(ValueError, match="sync must be one of"):
        vault.sync(sync_type="always")


# ===========================================================================
# list_items
# ===========================================================================

def test_list_items(vault):
    mock_output = "123|Site A|userA\n456|Site B|userB"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = mock_output
        items = vault.list_items()
        assert len(items) == 2
        assert items[0] == {"id": "123", "name": "Site A", "username": "userA"}
        assert items[1] == {"id": "456", "name": "Site B", "username": "userB"}


def test_list_items_empty(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        assert vault.list_items() == []


def test_list_items_malformed(vault):
    mock_output = "123|Site A\n456|Site B|userB|extra"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = mock_output
        items = vault.list_items()
        assert items == []


def test_list_items_error(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Failed")
        assert vault.list_items() == []


def test_list_items_with_group(vault):
    mock_output = "123|Site A|userA"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = mock_output
        items = vault.list_items(group="Work")
        args, _ = mock_run_cmd.call_args
        assert "Work" in args[0]
        assert items[0] == {"id": "123", "name": "Site A", "username": "userA"}


def test_list_items_long_format(vault):
    mock_output = "123|Site A|userA|2024-01-15"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = mock_output
        items = vault.list_items(long=True)
        assert len(items) == 1
        assert items[0]["last_modified"] == "2024-01-15"
        assert "id" in items[0]
        assert "name" in items[0]
        assert "username" in items[0]


def test_list_items_last_touch(vault):
    mock_output = "123|Site A|userA|2024-02-20"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = mock_output
        items = vault.list_items(last_touch=True)
        assert len(items) == 1
        assert items[0]["last_touch"] == "2024-02-20"


def test_list_items_long_and_last_touch(vault):
    mock_output = "123|Site A|userA|2024-01-15|2024-02-20"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = mock_output
        items = vault.list_items(long=True, last_touch=True)
        assert len(items) == 1
        assert items[0]["last_modified"] == "2024-01-15"
        assert items[0]["last_touch"] == "2024-02-20"


def test_list_items_long_malformed_skips_short_line(vault):
    """A 3-part line is invalid when long=True (expects 4 parts)."""
    mock_output = "123|Site A|userA"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = mock_output
        items = vault.list_items(long=True)
        assert items == []


# ===========================================================================
# get_password
# ===========================================================================

def test_get_password_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "secret_password"
        assert vault.get_password("123") == "secret_password"
        mock_run_cmd.assert_called_once_with(["show", "--password", "123"])


def test_get_password_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Item not found")
        assert vault.get_password("999") is None


# ===========================================================================
# get_url
# ===========================================================================

def test_get_url_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "https://example.com"
        assert vault.get_url("123") == "https://example.com"
        mock_run_cmd.assert_called_once_with(["show", "--url", "123"])


def test_get_url_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not found")
        assert vault.get_url("999") is None


# ===========================================================================
# get_notes
# ===========================================================================

def test_get_notes_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "Some private notes"
        assert vault.get_notes("123") == "Some private notes"
        mock_run_cmd.assert_called_once_with(["show", "--notes", "123"])


def test_get_notes_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not found")
        assert vault.get_notes("999") is None


# ===========================================================================
# get_field
# ===========================================================================

def test_get_field_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "field_value"
        result = vault.get_field("123", "myfield")
        assert result == "field_value"
        mock_run_cmd.assert_called_once_with(
            ["show", "--field=myfield", "123"]
        )


def test_get_field_name_embedded_in_single_token(vault):
    """Field name must be part of a single --field=<name> token, not split."""
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "val"
        vault.get_field("123", "custom field")
        args, _ = mock_run_cmd.call_args
        cmd_args = args[0]
        # The field name must be joined with --field= in one token.
        assert "--field=custom field" in cmd_args
        # It must not appear as a separate element.
        assert "custom field" not in [a for a in cmd_args if not a.startswith("--field=")]


def test_get_field_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not found")
        assert vault.get_field("999", "myfield") is None


# ===========================================================================
# get_item_json
# ===========================================================================

def test_get_item_json_success(vault):
    payload = [{"id": "123", "name": "Site A", "username": "user"}]
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = json.dumps(payload)
        result = vault.get_item_json("123")
        assert result == payload
        args, _ = mock_run_cmd.call_args
        assert "--json" in args[0]


def test_get_item_json_with_sync(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "[]"
        vault.get_item_json("123", sync="now")
        args, _ = mock_run_cmd.call_args
        assert "--sync=now" in args[0]


def test_get_item_json_with_expand_multi(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "[]"
        vault.get_item_json("site", expand_multi=True)
        args, _ = mock_run_cmd.call_args
        assert "--expand-multi" in args[0]


def test_get_item_json_invalid_sync(vault):
    with pytest.raises(ValueError):
        vault.get_item_json("123", sync="bad")


def test_get_item_json_lpass_error(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not found")
        assert vault.get_item_json("999") is None


def test_get_item_json_invalid_json_returns_none(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "this is not json"
        assert vault.get_item_json("123") is None


# ===========================================================================
# get_item_details
# ===========================================================================

def test_get_item_details(vault):
    mock_output = "ID: 123\nName: My Site\nUsername: myuser\nURL: https://example.com"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = mock_output
        details = vault.get_item_details("123")
        assert details["ID"] == "123"
        assert details["Name"] == "My Site"
        assert details["Username"] == "myuser"
        assert details["URL"] == "https://example.com"


def test_get_item_details_empty(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        assert vault.get_item_details("123") == {}


def test_get_item_details_malformed(vault):
    mock_output = "No colon here\nKey: Value"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = mock_output
        details = vault.get_item_details("123")
        assert "Key" in details
        assert details["Key"] == "Value"
        assert len(details) == 1


def test_get_item_details_error(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Failed")
        assert vault.get_item_details("123") == {}


# ===========================================================================
# search
# ===========================================================================

def test_search(vault):
    mock_items = [
        {"id": "1", "name": "Google", "username": "user1"},
        {"id": "2", "name": "GitHub", "username": "user2"},
        {"id": "3", "name": "GitLab", "username": "user1"}
    ]
    with patch.object(LastPassVault, "list_items") as mock_list:
        mock_list.return_value = mock_items

        results = vault.search("Git")
        assert len(results) == 2
        assert results[0]["name"] == "GitHub"
        assert results[1]["name"] == "GitLab"

        results = vault.search("user2")
        assert len(results) == 1
        assert results[0]["id"] == "2"

        assert vault.search("unknown") == []


# ===========================================================================
# add
# ===========================================================================

def test_add_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        assert vault.add("Work/Email") is True
        args, _ = mock_run_cmd.call_args
        assert "--non-interactive" in args[0]
        assert "Work/Email" in args[0]


def test_add_with_template_via_stdin(vault):
    """Entry content (including passwords) must be passed via input_data."""
    template = "Username: alice\nPassword: s3cret\nURL: https://example.com"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.add("Work/Email", template=template)
        args, kwargs = mock_run_cmd.call_args
        # Template must reach lpass via stdin.
        assert kwargs.get("input_data") == template
        # The template string (containing the password) must not be in argv.
        assert "s3cret" not in " ".join(args[0])


def test_add_with_sync(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.add("Work/Email", sync="now")
        args, _ = mock_run_cmd.call_args
        assert "--sync=now" in args[0]


def test_add_invalid_sync(vault):
    with pytest.raises(ValueError):
        vault.add("Work/Email", sync="bad")


def test_add_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not logged in")
        assert vault.add("Work/Email") is False


# ===========================================================================
# edit
# ===========================================================================

def test_edit_password_via_stdin(vault):
    """Password field value must never appear in argv."""
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.edit("123", "password", "newpassword")
        args, kwargs = mock_run_cmd.call_args
        # Value must be in input_data, not in argv.
        assert kwargs.get("input_data") == "newpassword"
        assert "newpassword" not in " ".join(args[0])


def test_edit_password_uses_builtin_flag(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.edit("123", "password", "newpassword")
        args, _ = mock_run_cmd.call_args
        assert "--password" in args[0]


def test_edit_username(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.edit("123", "username", "alice")
        args, _ = mock_run_cmd.call_args
        assert "--username" in args[0]


def test_edit_url(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.edit("123", "url", "https://example.com")
        args, _ = mock_run_cmd.call_args
        assert "--url" in args[0]


def test_edit_custom_field(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.edit("123", "pin", "4321")
        args, kwargs = mock_run_cmd.call_args
        assert "--field=pin" in args[0]
        # Value still goes via stdin.
        assert kwargs.get("input_data") == "4321"


def test_edit_value_always_via_stdin(vault):
    """For every field type, the value must go through input_data, not argv."""
    for field in ("username", "password", "url", "notes", "name", "custom"):
        with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
            mock_run_cmd.return_value = ""
            vault.edit("123", field, "sensitive_value")
            _, kwargs = mock_run_cmd.call_args
            assert kwargs.get("input_data") == "sensitive_value", (
                f"Value for field '{field}' not in input_data"
            )


def test_edit_with_sync(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.edit("123", "notes", "note text", sync="now")
        args, _ = mock_run_cmd.call_args
        assert "--sync=now" in args[0]


def test_edit_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not found")
        assert vault.edit("999", "password", "x") is False


# ===========================================================================
# mv
# ===========================================================================

def test_mv_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        assert vault.mv("123", "Personal") is True
        args, _ = mock_run_cmd.call_args
        assert "mv" in args[0]
        assert "123" in args[0]
        assert "Personal" in args[0]


def test_mv_with_sync(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.mv("123", "Personal", sync="now")
        args, _ = mock_run_cmd.call_args
        assert "--sync=now" in args[0]


def test_mv_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not found")
        assert vault.mv("999", "Group") is False


def test_mv_invalid_sync(vault):
    with pytest.raises(ValueError):
        vault.mv("123", "Group", sync="bad")


# ===========================================================================
# rm
# ===========================================================================

def test_rm_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        assert vault.rm("123") is True
        args, _ = mock_run_cmd.call_args
        assert "rm" in args[0]
        assert "123" in args[0]


def test_rm_with_sync(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.rm("123", sync="now")
        args, _ = mock_run_cmd.call_args
        assert "--sync=now" in args[0]


def test_rm_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not found")
        assert vault.rm("999") is False


def test_rm_invalid_sync(vault):
    with pytest.raises(ValueError):
        vault.rm("123", sync="bad")


# ===========================================================================
# duplicate
# ===========================================================================

def test_duplicate_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        assert vault.duplicate("123") is True
        args, _ = mock_run_cmd.call_args
        assert "duplicate" in args[0]
        assert "123" in args[0]


def test_duplicate_with_sync(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.duplicate("123", sync="now")
        args, _ = mock_run_cmd.call_args
        assert "--sync=now" in args[0]


def test_duplicate_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not found")
        assert vault.duplicate("999") is False


# ===========================================================================
# generate
# ===========================================================================

def test_generate_success(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "Xk9#mP2!qL"
        result = vault.generate("Work/Email", 20)
        assert result == "Xk9#mP2!qL"
        args, _ = mock_run_cmd.call_args
        assert "generate" in args[0]
        assert "Work/Email" in args[0]
        assert "20" in args[0]


def test_generate_invalid_length_zero(vault):
    with pytest.raises(ValueError, match="length must be a positive integer"):
        vault.generate("Work/Email", 0)


def test_generate_invalid_length_negative(vault):
    with pytest.raises(ValueError, match="length must be a positive integer"):
        vault.generate("Work/Email", -5)


def test_generate_no_symbols(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "AbCdEfGhIj12"
        vault.generate("Work/Email", 12, no_symbols=True)
        args, _ = mock_run_cmd.call_args
        assert "--no-symbols" in args[0]


def test_generate_with_username_and_url(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "pass"
        vault.generate("Work/Email", 16,
                       username="alice", url="https://example.com")
        args, _ = mock_run_cmd.call_args
        assert "--username" in args[0]
        assert "alice" in args[0]
        assert "--url" in args[0]
        assert "https://example.com" in args[0]


def test_generate_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not logged in")
        assert vault.generate("Work/Email", 20) is None


def test_generate_with_sync(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "pass"
        vault.generate("Work/Email", 16, sync="now")
        args, _ = mock_run_cmd.call_args
        assert "--sync=now" in args[0]


def test_generate_invalid_sync(vault):
    with pytest.raises(ValueError):
        vault.generate("Work/Email", 16, sync="bad")


# ===========================================================================
# export
# ===========================================================================

def test_export_returns_csv(vault):
    csv_content = "url,username,password\nhttps://example.com,alice,s3cret"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = csv_content
        result = vault.export()
        assert result == csv_content
        args, _ = mock_run_cmd.call_args
        assert "export" in args[0]


def test_export_with_fields(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "url,username\nhttps://example.com,alice"
        vault.export(fields=["url", "username"])
        args, _ = mock_run_cmd.call_args
        assert "--fields" in args[0]
        # The field names must be joined as a comma-separated string.
        fields_idx = args[0].index("--fields")
        assert args[0][fields_idx + 1] == "url,username"


def test_export_with_sync(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = "csv"
        vault.export(sync="now")
        args, _ = mock_run_cmd.call_args
        assert "--sync=now" in args[0]


def test_export_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not logged in")
        assert vault.export() is None


def test_export_invalid_sync(vault):
    with pytest.raises(ValueError):
        vault.export(sync="bad")


# ===========================================================================
# import_vault
# ===========================================================================

def test_import_vault_success(vault):
    csv_data = "url,username,password\nhttps://example.com,alice,s3cret"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        assert vault.import_vault(csv_data) is True
        args, kwargs = mock_run_cmd.call_args
        assert "import" in args[0]
        # CSV data must arrive via stdin.
        assert kwargs.get("input_data") == csv_data


def test_import_vault_data_not_in_argv(vault):
    """CSV data (containing passwords) must never appear in the argv list."""
    csv_data = "url,username,password\nhttps://example.com,alice,s3cret"
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.import_vault(csv_data)
        args, kwargs = mock_run_cmd.call_args
        assert "s3cret" not in " ".join(args[0]), (
            "Sensitive CSV data (including passwords) leaked into argv"
        )
        assert kwargs.get("input_data") == csv_data


def test_import_vault_keep_dupes(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.import_vault("csv", keep_dupes=True)
        args, _ = mock_run_cmd.call_args
        assert "--keep-dupes" in args[0]


def test_import_vault_with_sync(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.return_value = ""
        vault.import_vault("csv", sync="now")
        args, _ = mock_run_cmd.call_args
        assert "--sync=now" in args[0]


def test_import_vault_failure(vault):
    with patch.object(LastPassVault, "_run_command") as mock_run_cmd:
        mock_run_cmd.side_effect = LastPassError("Not logged in")
        assert vault.import_vault("csv") is False


def test_import_vault_invalid_sync(vault):
    with pytest.raises(ValueError):
        vault.import_vault("csv", sync="bad")
