import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
import yaml

from nebula_cert_manager.cli import build_parser, main
from nebula_cert_manager.models import (
    CAInfo,
    ClientInfo,
    Registry,
)


@pytest.fixture
def sample_registry_with_client():
    return Registry(
        ca=CAInfo(
            name="test-ca",
            cert="ca-cert\n",
            key="ca-key\n",
            fingerprint="ca-fp",
            created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
        ),
        clients={
            "laptop": [
                ClientInfo(
                    fingerprint="client-fp",
                    cert="client-cert\n",
                    key="client-key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
            ],
        },
    )


def test_parser_global_options():
    parser = build_parser()
    args = parser.parse_args(
        [
            "--registry-dir",
            "/tmp/reg",
            "--nebula-cert",
            "/usr/bin/nebula-cert",
            "--sops",
            "/usr/bin/sops",
            "info",
        ]
    )
    assert args.registry_dir == Path("/tmp/reg")
    assert args.nebula_cert == "/usr/bin/nebula-cert"
    assert args.sops == "/usr/bin/sops"
    assert args.command == "info"


def test_parser_age_option():
    parser = build_parser()
    args = parser.parse_args(["--age", "age1testkey", "info"])
    assert args.age == "age1testkey"


def test_parser_init_command():
    parser = build_parser()
    args = parser.parse_args(["init"])
    assert args.command == "init"


def test_parser_issue_command():
    parser = build_parser()
    args = parser.parse_args(["issue", "--name", "laptop"])
    assert args.command == "issue"
    assert args.name == "laptop"


def test_parser_list_command():
    parser = build_parser()
    args = parser.parse_args(["list", "--all", "--json"])
    assert args.command == "list"
    assert args.show_all is True
    assert args.output_json is True


def test_no_command_prints_help(capsys):
    with pytest.raises(SystemExit) as exc_info:
        main([])
    assert exc_info.value.code == 1


@patch("nebula_cert_manager.commands.list_cmd.run")
@patch("nebula_cert_manager.registry.RegistryManager")
@patch("nebula_cert_manager.sops.Sops")
@patch("nebula_cert_manager.pki.PKI")
def test_main_dispatches_to_command(mock_pki, mock_sops, mock_reg_mgr, mock_list_run):
    """Test that main dispatches to the correct command function."""
    main(["--registry-dir", "/tmp/test", "list"])
    mock_list_run.assert_called_once()


def test_list_command_table_output(sample_registry_with_client, tmp_path, capsys):
    """Test list command produces table output."""
    from nebula_cert_manager.commands import list_cmd
    import argparse

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()

    # Write a nebula.config.yml so list can cross-reference IPs
    nebula_config = {
        "network": {"cidr": "10.43.0.0/16"},
        "hosts": {
            "hosts": {
                "laptop": {"nebula_ip": "10.43.1.1"},
            },
        },
    }
    (registry_dir / "nebula.config.yml").write_text(yaml.dump(nebula_config))

    args = argparse.Namespace(
        registry_mgr=MagicMock(),
        show_all=False,
        revoked=False,
        output_json=False,
    )
    args.registry_mgr.exists.return_value = True
    args.registry_mgr.load.return_value = sample_registry_with_client
    args.registry_mgr.registry_dir = registry_dir

    list_cmd.run(args)

    captured = capsys.readouterr()
    assert "laptop" in captured.out
    assert "10.43.1.1" in captured.out
    assert "ACTIVE" in captured.out
    assert "FINGERPRINT" in captured.out
    assert "client-fp" in captured.out


def test_list_command_json_output(sample_registry_with_client, tmp_path, capsys):
    from nebula_cert_manager.commands import list_cmd
    import argparse

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()

    args = argparse.Namespace(
        registry_mgr=MagicMock(),
        show_all=False,
        revoked=False,
        output_json=True,
    )
    args.registry_mgr.exists.return_value = True
    args.registry_mgr.load.return_value = sample_registry_with_client
    args.registry_mgr.registry_dir = registry_dir

    list_cmd.run(args)

    captured = capsys.readouterr()
    data = json.loads(captured.out)
    assert "laptop" in data
    assert isinstance(data["laptop"], list)
    assert len(data["laptop"]) == 1


def test_revoke_command(sample_registry_with_client):
    from nebula_cert_manager.commands import revoke
    import argparse

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = sample_registry_with_client

    args = argparse.Namespace(registry_mgr=mock_mgr, fingerprint="client-fp")
    revoke.run(args)

    assert sample_registry_with_client.clients["laptop"][0].revoked is True
    assert sample_registry_with_client.clients["laptop"][0].revoked_at is not None
    mock_mgr.save.assert_called_once()


def test_revoke_command_not_found(sample_registry_with_client):
    from nebula_cert_manager.commands import revoke
    import argparse

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = sample_registry_with_client

    args = argparse.Namespace(registry_mgr=mock_mgr, fingerprint="nonexistent-fp")
    with pytest.raises(SystemExit):
        revoke.run(args)


def test_revoke_command_already_revoked(sample_registry_with_client):
    from nebula_cert_manager.commands import revoke
    import argparse

    sample_registry_with_client.clients["laptop"][0].revoked = True
    sample_registry_with_client.clients["laptop"][0].revoked_at = datetime(
        2026, 6, 1, tzinfo=timezone.utc
    )

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = sample_registry_with_client

    args = argparse.Namespace(registry_mgr=mock_mgr, fingerprint="client-fp")
    with pytest.raises(SystemExit):
        revoke.run(args)


def test_config_command(sample_registry_with_client, tmp_path):
    from nebula_cert_manager.commands import config
    import argparse

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = sample_registry_with_client
    mock_mgr.registry_dir = registry_dir

    args = argparse.Namespace(
        registry_mgr=mock_mgr,
        name="laptop",
        fingerprint=None,
        output_dir=tmp_path,
        template=None,
    )
    config.run(args)

    out_dir = tmp_path / "laptop"
    assert (out_dir / "client.crt").exists()
    assert (out_dir / "client.key").exists()
    assert (out_dir / "config.yml").exists()

    config_text = (out_dir / "config.yml").read_text()
    assert "ca-cert" in config_text
    assert "client-cert" in config_text


def test_config_command_with_fingerprint(sample_registry_with_client, tmp_path):
    from nebula_cert_manager.commands import config
    import argparse

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = sample_registry_with_client
    mock_mgr.registry_dir = registry_dir

    args = argparse.Namespace(
        registry_mgr=mock_mgr,
        name="laptop",
        fingerprint="client-fp",
        output_dir=tmp_path,
        template=None,
    )
    config.run(args)

    out_dir = tmp_path / "laptop"
    assert (out_dir / "client.crt").exists()


def test_config_command_fingerprint_wrong_name(sample_registry_with_client):
    from nebula_cert_manager.commands import config
    import argparse

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = sample_registry_with_client

    args = argparse.Namespace(
        registry_mgr=mock_mgr,
        name="other-host",
        fingerprint="client-fp",
        output_dir=Path("/tmp"),
        template=None,
    )
    with pytest.raises(SystemExit):
        config.run(args)


def test_config_command_includes_blocklist(tmp_path):
    from nebula_cert_manager.commands import config
    import argparse

    registry = Registry(
        ca=CAInfo(
            name="test-ca",
            cert="ca-cert\n",
            key="ca-key\n",
            fingerprint="ca-fp",
            created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
        ),
        clients={
            "laptop": [
                ClientInfo(
                    fingerprint="active-fp",
                    cert="client-cert\n",
                    key="client-key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
            ],
            "server": [
                ClientInfo(
                    fingerprint="revoked-fp-1",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                    revoked=True,
                    revoked_at=datetime(2026, 6, 1, tzinfo=timezone.utc),
                ),
                ClientInfo(
                    fingerprint="revoked-fp-2",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2026, 2, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 2, 1, tzinfo=timezone.utc),
                    revoked=True,
                    revoked_at=datetime(2026, 7, 1, tzinfo=timezone.utc),
                ),
            ],
        },
    )

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    args = argparse.Namespace(
        registry_mgr=mock_mgr,
        name="laptop",
        fingerprint=None,
        output_dir=tmp_path,
        template=None,
    )
    config.run(args)

    config_text = (tmp_path / "laptop" / "config.yml").read_text()
    assert "blocklist:" in config_text
    assert "revoked-fp-1" in config_text
    assert "revoked-fp-2" in config_text


def test_config_command_no_blocklist_when_none_revoked(
    sample_registry_with_client, tmp_path
):
    from nebula_cert_manager.commands import config
    import argparse

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = sample_registry_with_client
    mock_mgr.registry_dir = registry_dir

    args = argparse.Namespace(
        registry_mgr=mock_mgr,
        name="laptop",
        fingerprint=None,
        output_dir=tmp_path,
        template=None,
    )
    config.run(args)

    config_text = (tmp_path / "laptop" / "config.yml").read_text()
    assert "blocklist" not in config_text


def test_config_command_no_active_cert():
    from nebula_cert_manager.commands import config
    import argparse

    registry = Registry(
        ca=CAInfo(
            name="test-ca",
            cert="ca-cert\n",
            key="ca-key\n",
            fingerprint="ca-fp",
            created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
        ),
        clients={
            "laptop": [
                ClientInfo(
                    fingerprint="revoked-fp",
                    cert="client-cert\n",
                    key="client-key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                    revoked=True,
                    revoked_at=datetime(2026, 6, 1, tzinfo=timezone.utc),
                ),
            ],
        },
    )

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry

    args = argparse.Namespace(
        registry_mgr=mock_mgr,
        name="laptop",
        fingerprint=None,
        output_dir=Path("/tmp"),
        template=None,
    )
    with pytest.raises(SystemExit):
        config.run(args)


def test_delete_command(sample_registry_with_client):
    from nebula_cert_manager.commands import delete
    import argparse

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = sample_registry_with_client

    args = argparse.Namespace(registry_mgr=mock_mgr, fingerprint="client-fp")
    delete.run(args)

    assert "laptop" not in sample_registry_with_client.clients
    mock_mgr.save.assert_called_once()


def test_delete_command_not_found(sample_registry_with_client):
    from nebula_cert_manager.commands import delete
    import argparse

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = sample_registry_with_client

    args = argparse.Namespace(registry_mgr=mock_mgr, fingerprint="nonexistent-fp")
    with pytest.raises(SystemExit):
        delete.run(args)


def test_delete_command_keeps_other_certs():
    from nebula_cert_manager.commands import delete
    import argparse

    registry = Registry(
        ca=CAInfo(
            name="test-ca",
            cert="ca-cert\n",
            key="ca-key\n",
            fingerprint="ca-fp",
            created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
        ),
        clients={
            "laptop": [
                ClientInfo(
                    fingerprint="fp-1",
                    cert="cert-1\n",
                    key="key-1\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
                ClientInfo(
                    fingerprint="fp-2",
                    cert="cert-2\n",
                    key="key-2\n",
                    issued_at=datetime(2026, 6, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 6, 1, tzinfo=timezone.utc),
                ),
            ],
        },
    )

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry

    args = argparse.Namespace(registry_mgr=mock_mgr, fingerprint="fp-1")
    delete.run(args)

    assert "laptop" in registry.clients
    assert len(registry.clients["laptop"]) == 1
    assert registry.clients["laptop"][0].fingerprint == "fp-2"


def test_info_command(sample_registry_with_client, tmp_path, capsys):
    from nebula_cert_manager.commands import info
    import argparse

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()

    # Write a nebula.config.yml so info can display network info
    nebula_config = {
        "network": {"cidr": "10.43.0.0/16"},
        "hosts": {
            "hosts": {
                "server1": {
                    "nebula_ip": "10.43.0.1",
                    "public_endpoints": ["203.0.113.1"],
                    "listen_port": 44300,
                },
            },
        },
    }
    (registry_dir / "nebula.config.yml").write_text(yaml.dump(nebula_config))

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = sample_registry_with_client
    mock_mgr.registry_dir = registry_dir

    args = argparse.Namespace(registry_mgr=mock_mgr)
    info.run(args)

    captured = capsys.readouterr()
    assert "test-ca" in captured.out
    assert "10.43.0.0/16" in captured.out
    assert "Active:" in captured.out


# --- Sync command tests ---


def _make_nebula_config_yaml(hosts_dict, firewall=None):
    cfg = {
        "network": {"cidr": "10.43.0.0/16"},
        "hosts": {"hosts": hosts_dict},
    }
    if firewall is not None:
        cfg["firewall"] = firewall
    return yaml.dump(cfg)


def _make_registry(clients=None):
    return Registry(
        ca=CAInfo(
            name="test-ca",
            cert="ca-cert\n",
            key="ca-key\n",
            fingerprint="ca-fp",
            created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
        ),
        clients=clients or {},
    )


def _sync_args(mock_mgr, mock_pki=None, **overrides):
    defaults = {
        "registry_mgr": mock_mgr,
        "pki": mock_pki or MagicMock(),
        "expiry_threshold": 100,
        "no_revoke_unknown": False,
        "no_revoke_old": False,
        "duration": None,
        "dry_run": False,
    }
    defaults.update(overrides)
    return argparse.Namespace(**defaults)


def _setup_pki_mock(pki_mock):
    pki_mock.sign.return_value = ("new-cert-pem\n", "new-key-pem\n")
    pki_mock.print_cert.return_value = {
        "fingerprint": "new-fp",
        "details": {"notAfter": "2027-06-01T00:00:00Z"},
    }
    pki_mock.sign_and_build_client_info.return_value = ClientInfo(
        fingerprint="new-fp",
        cert="new-cert-pem\n",
        key="new-key-pem\n",
        issued_at=datetime(2026, 2, 1, tzinfo=timezone.utc),
        expires_at=datetime(2027, 6, 1, tzinfo=timezone.utc),
    )
    return pki_mock


def test_sync_issues_cert_for_host_with_no_active_cert(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml({"server1": {"nebula_ip": "10.43.1.1"}})
    )

    registry = _make_registry()
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = _setup_pki_mock(MagicMock())
    args = _sync_args(mock_mgr, mock_pki)
    sync.run(args)

    captured = capsys.readouterr()
    assert "issue" in captured.out
    assert "server1" in captured.out
    assert "no active cert" in captured.out
    assert "1 issued" in captured.out
    mock_pki.sign_and_build_client_info.assert_called_once()
    mock_mgr.save.assert_called_once()
    assert len(registry.clients["server1"]) == 1


def test_sync_issues_cert_for_expiring_host(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml({"laptop": {"nebula_ip": "10.43.1.2"}})
    )

    registry = _make_registry(
        clients={
            "laptop": [
                ClientInfo(
                    fingerprint="old-fp",
                    cert="old-cert\n",
                    key="old-key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2026, 4, 15, tzinfo=timezone.utc),
                ),
            ],
        }
    )
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = _setup_pki_mock(MagicMock())
    args = _sync_args(mock_mgr, mock_pki)
    sync.run(args)

    captured = capsys.readouterr()
    assert "issue" in captured.out
    assert "expires 2026-04-15" in captured.out
    assert "1 issued" in captured.out


def test_sync_skips_host_with_valid_cert(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml({"server1": {"nebula_ip": "10.43.1.1"}})
    )

    registry = _make_registry(
        clients={
            "server1": [
                ClientInfo(
                    fingerprint="valid-fp",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
            ],
        }
    )
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = MagicMock()
    args = _sync_args(mock_mgr, mock_pki)
    sync.run(args)

    captured = capsys.readouterr()
    assert "skip" in captured.out
    assert "1 skipped" in captured.out
    mock_pki.sign_and_build_client_info.assert_not_called()
    mock_mgr.save.assert_not_called()


def test_sync_revokes_unknown_hosts(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml({"server1": {"nebula_ip": "10.43.1.1"}})
    )

    registry = _make_registry(
        clients={
            "server1": [
                ClientInfo(
                    fingerprint="valid-fp",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
            ],
            "old-server": [
                ClientInfo(
                    fingerprint="unknown-fp",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
            ],
        }
    )
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = MagicMock()
    args = _sync_args(mock_mgr, mock_pki)
    sync.run(args)

    captured = capsys.readouterr()
    assert "revoke" in captured.out
    assert "old-server" in captured.out
    assert "host not in config" in captured.out
    assert "1 revoked" in captured.out
    assert registry.clients["old-server"][0].revoked is True
    assert registry.clients["old-server"][0].revoked_at is not None
    mock_mgr.save.assert_called_once()


def test_sync_no_revoke_unknown_flag(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml({"server1": {"nebula_ip": "10.43.1.1"}})
    )

    registry = _make_registry(
        clients={
            "server1": [
                ClientInfo(
                    fingerprint="valid-fp",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
            ],
            "old-server": [
                ClientInfo(
                    fingerprint="unknown-fp",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
            ],
        }
    )
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = MagicMock()
    args = _sync_args(mock_mgr, mock_pki, no_revoke_unknown=True)
    sync.run(args)

    captured = capsys.readouterr()
    assert "host not in config" not in captured.out
    assert "0 revoked" in captured.out
    assert registry.clients["old-server"][0].revoked is False


def test_sync_revokes_old_certs_keeps_newest(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml({"laptop": {"nebula_ip": "10.43.1.1"}})
    )

    registry = _make_registry(
        clients={
            "laptop": [
                ClientInfo(
                    fingerprint="old-fp",
                    cert="old-cert\n",
                    key="old-key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
                ClientInfo(
                    fingerprint="new-fp",
                    cert="new-cert\n",
                    key="new-key\n",
                    issued_at=datetime(2026, 6, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 6, 1, tzinfo=timezone.utc),
                ),
            ],
        }
    )
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = MagicMock()
    args = _sync_args(mock_mgr, mock_pki)
    sync.run(args)

    captured = capsys.readouterr()
    assert "revoke" in captured.out
    assert "old-fp" in captured.out
    assert "superseded by newer cert" in captured.out
    # The older cert (old-fp) should be revoked, the newer (new-fp) kept
    old_cert = [c for c in registry.clients["laptop"] if c.fingerprint == "old-fp"][0]
    new_cert = [c for c in registry.clients["laptop"] if c.fingerprint == "new-fp"][0]
    assert old_cert.revoked is True
    assert new_cert.revoked is False


def test_sync_no_revoke_old_flag(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml({"laptop": {"nebula_ip": "10.43.1.1"}})
    )

    registry = _make_registry(
        clients={
            "laptop": [
                ClientInfo(
                    fingerprint="old-fp",
                    cert="old-cert\n",
                    key="old-key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
                ClientInfo(
                    fingerprint="new-fp",
                    cert="new-cert\n",
                    key="new-key\n",
                    issued_at=datetime(2026, 6, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 6, 1, tzinfo=timezone.utc),
                ),
            ],
        }
    )
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = MagicMock()
    args = _sync_args(mock_mgr, mock_pki, no_revoke_old=True)
    sync.run(args)

    captured = capsys.readouterr()
    assert "superseded" not in captured.out
    old_cert = [c for c in registry.clients["laptop"] if c.fingerprint == "old-fp"][0]
    assert old_cert.revoked is False


def test_sync_dry_run(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml(
            {
                "server1": {"nebula_ip": "10.43.1.1"},
            }
        )
    )

    registry = _make_registry()
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = MagicMock()
    args = _sync_args(mock_mgr, mock_pki, dry_run=True)
    sync.run(args)

    captured = capsys.readouterr()
    assert "[dry run]" in captured.out
    assert "(dry run)" in captured.out
    mock_pki.sign_and_build_client_info.assert_not_called()
    mock_mgr.save.assert_not_called()
    assert "server1" not in registry.clients


def test_sync_no_changes_no_save(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml({"server1": {"nebula_ip": "10.43.1.1"}})
    )

    registry = _make_registry(
        clients={
            "server1": [
                ClientInfo(
                    fingerprint="valid-fp",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
            ],
        }
    )
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = MagicMock()
    args = _sync_args(mock_mgr, mock_pki)
    sync.run(args)

    mock_mgr.save.assert_not_called()


def test_sync_mixed_scenario(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml(
            {
                "server1": {"nebula_ip": "10.43.1.1"},
                "expiring": {"nebula_ip": "10.43.1.2"},
            }
        )
    )

    registry = _make_registry(
        clients={
            "server1": [
                ClientInfo(
                    fingerprint="valid-fp",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
            ],
            "expiring": [
                ClientInfo(
                    fingerprint="exp-fp",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2025, 6, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2026, 3, 1, tzinfo=timezone.utc),
                ),
            ],
            "unknown-host": [
                ClientInfo(
                    fingerprint="unk-fp",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
                ),
            ],
        }
    )
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = _setup_pki_mock(MagicMock())
    args = _sync_args(mock_mgr, mock_pki)
    sync.run(args)

    captured = capsys.readouterr()
    assert "skip" in captured.out
    assert "server1" in captured.out
    assert "1 issued" in captured.out
    assert "1 skipped" in captured.out
    # unknown-host revoked + old expiring cert revoked = 2 revoked
    assert "2 revoked" in captured.out


def test_sync_passes_duration_to_pki(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml({"server1": {"nebula_ip": "10.43.1.1"}})
    )

    registry = _make_registry()
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = _setup_pki_mock(MagicMock())
    args = _sync_args(mock_mgr, mock_pki, duration="8760h")
    sync.run(args)

    call_kwargs = mock_pki.sign_and_build_client_info.call_args
    assert call_kwargs.kwargs.get("duration") == "8760h" or (
        call_kwargs[1].get("duration") == "8760h" if len(call_kwargs) > 1 else False
    )


def test_sync_passes_groups_from_firewall(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml(
            {"server1": {"nebula_ip": "10.43.1.1"}},
            firewall={"host_groups": {"server1": ["web", "db"]}},
        )
    )

    registry = _make_registry()
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = _setup_pki_mock(MagicMock())
    args = _sync_args(mock_mgr, mock_pki)
    sync.run(args)

    call_kwargs = mock_pki.sign_and_build_client_info.call_args
    assert call_kwargs.kwargs.get("groups") == ["web", "db"] or (
        call_kwargs[1].get("groups") == ["web", "db"] if len(call_kwargs) > 1 else False
    )


def test_sync_parser_defaults():
    parser = build_parser()
    args = parser.parse_args(["sync"])
    assert args.command == "sync"
    assert args.expiry_threshold == 100
    assert args.no_revoke_unknown is False
    assert args.no_revoke_old is False
    assert args.duration is None
    assert args.dry_run is False


def test_sync_registry_not_found(tmp_path, capsys):
    from nebula_cert_manager.commands import sync

    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = False

    args = _sync_args(mock_mgr)
    with pytest.raises(SystemExit) as exc_info:
        sync.run(args)
    assert exc_info.value.code == 1


def test_sync_boundary_cert_expiring_at_threshold(tmp_path, capsys):
    from nebula_cert_manager.commands import sync
    from datetime import timedelta

    registry_dir = tmp_path / "registry"
    registry_dir.mkdir()
    (registry_dir / "nebula.config.yml").write_text(
        _make_nebula_config_yaml({"server1": {"nebula_ip": "10.43.1.1"}})
    )

    now = datetime.now(timezone.utc)
    # Cert expires exactly at the threshold boundary
    expires_at = now + timedelta(days=100)

    registry = _make_registry(
        clients={
            "server1": [
                ClientInfo(
                    fingerprint="boundary-fp",
                    cert="cert\n",
                    key="key\n",
                    issued_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                    expires_at=expires_at,
                ),
            ],
        }
    )
    mock_mgr = MagicMock()
    mock_mgr.exists.return_value = True
    mock_mgr.load.return_value = registry
    mock_mgr.registry_dir = registry_dir

    mock_pki = _setup_pki_mock(MagicMock())
    args = _sync_args(mock_mgr, mock_pki, expiry_threshold=100)
    sync.run(args)

    captured = capsys.readouterr()
    # expires_at <= now + threshold means it should be reissued
    assert "issue" in captured.out
    assert "1 issued" in captured.out
