from contextlib import nullcontext as does_not_raise
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import ContextManager
from typing import Dict
from typing import Optional
from unittest.mock import Mock
from unittest.mock import patch

from pytest import mark
from pytest import raises

from libkeyringctl import sequoia
from libkeyringctl.types import Fingerprint
from libkeyringctl.types import PacketKind
from libkeyringctl.types import Uid
from libkeyringctl.types import Username


@mark.parametrize(
    "create_subdir, preserve_filename",
    [
        (False, True),
        (False, False),
        (True, True),
        (True, False),
    ],
)
@patch("libkeyringctl.sequoia.system")
@patch("libkeyringctl.sequoia.mkdtemp")
def test_keyring_split(mkdtemp_mock: Mock, system_mock: Mock, create_subdir: bool, preserve_filename: bool) -> None:
    with TemporaryDirectory() as tmp_dir_name:
        tmp_dir = Path(tmp_dir_name)

        keyring_tmp_dir = tmp_dir / "keyring"
        keyring_tmp_dir.mkdir()
        mkdtemp_mock.return_value = keyring_tmp_dir.absolute()

        if create_subdir:
            keyring_sub_dir = keyring_tmp_dir / "foo"
            keyring_sub_dir.mkdir()

        returned = sequoia.keyring_split(
            working_dir=tmp_dir,
            keyring=Path("foo"),
            preserve_filename=preserve_filename,
        )

        if create_subdir:
            assert returned == [keyring_sub_dir]
        else:
            assert returned == []


@mark.parametrize(
    "output",
    [
        None,
        Path("output"),
    ],
)
@patch("libkeyringctl.sequoia.system")
def test_keyring_merge(system_mock: Mock, output: Optional[Path]) -> None:
    certificates = [Path("foo"), Path("bar")]
    system_mock.return_value = "return"

    assert sequoia.keyring_merge(certificates=certificates, output=output) == "return"

    name, args, kwargs = system_mock.mock_calls[0]
    for cert in certificates:
        assert str(cert) in args[0]
    if output:
        assert "--output" in args[0] and str(output) in args[0]


@patch("libkeyringctl.sequoia.system")
@patch("libkeyringctl.sequoia.mkdtemp")
def test_packet_split(mkdtemp_mock: Mock, system_mock: Mock) -> None:
    certificate = Path("certificate")
    with TemporaryDirectory() as tmp_dir_name:
        tmp_dir = Path(tmp_dir_name)

        keyring_tmp_dir = tmp_dir / "keyring"
        keyring_tmp_dir.mkdir()
        mkdtemp_mock.return_value = keyring_tmp_dir.absolute()
        keyring_sub_dir = keyring_tmp_dir / "foo"
        keyring_sub_dir.mkdir()

        assert sequoia.packet_split(working_dir=tmp_dir, certificate=certificate) == [keyring_sub_dir]
        name, args, kwargs = system_mock.mock_calls[0]
        assert str(certificate) == args[0][-1]


@mark.parametrize("output, force", [(None, True), (None, False), (Path("output"), True), (Path("output"), False)])
@patch("libkeyringctl.sequoia.system")
def test_packet_join(system_mock: Mock, output: Optional[Path], force: bool) -> None:
    packets = [Path("packet1"), Path("packet2")]
    system_return = "return"
    system_mock.return_value = system_return

    assert sequoia.packet_join(packets, output=output, force=force) == system_return

    name, args, kwargs = system_mock.mock_calls[0]
    for packet in packets:
        assert str(packet) in args[0]
    if force:
        assert "--force" == args[0][1]
    if output:
        assert "--output" in args[0] and str(output) in args[0]


@mark.parametrize(
    "certifications_in_result, certifications, fingerprints",
    [
        ("something: 0123456789123456789012345678901234567890\n", True, None),
        ("something: 0123456789123456789012345678901234567890\n", False, None),
        (
            "something: 0123456789123456789012345678901234567890\n",
            True,
            {Fingerprint("0123456789123456789012345678901234567890"): Username("foo")},
        ),
        (
            "something: 0123456789123456789012345678901234567890\n",
            False,
            {Fingerprint("0123456789123456789012345678901234567890"): Username("foo")},
        ),
        (
            "something: 5678901234567890\n",
            True,
            {Fingerprint("0123456789123456789012345678901234567890"): Username("foo")},
        ),
        (
            "something: 5678901234567890\n",
            False,
            {Fingerprint("0123456789123456789012345678901234567890"): Username("foo")},
        ),
    ],
)
@patch("libkeyringctl.sequoia.system")
def test_inspect(
    system_mock: Mock,
    certifications_in_result: str,
    certifications: bool,
    fingerprints: Optional[Dict[Fingerprint, Username]],
) -> None:
    packet = Path("packet")
    result_header = "result\n"

    if certifications:
        system_mock.return_value = result_header + "\n" + certifications_in_result
    else:
        system_mock.return_value = result_header

    returned = sequoia.inspect(packet=packet, certifications=certifications, fingerprints=fingerprints)

    if fingerprints and certifications:
        for fingerprint, username in fingerprints.items():
            assert f"{fingerprint[24:]} {username}" in returned
    assert result_header in returned


@patch("libkeyringctl.sequoia.system")
def test_packet_dump(system_mock: Mock) -> None:
    system_mock.return_value = "return"
    assert sequoia.packet_dump(packet=Path("packet")) == "return"
    system_mock.called_once_with(["sq", "packet", "dump", "packet"])


@mark.parametrize(
    "packet_dump_return, query, result, expectation",
    [
        (
            """
Signature Packet
    Version: 4
    Type: SubkeyBinding
    Hash algo: SHA512
""",
            "Type",
            "SubkeyBinding",
            does_not_raise(),
        ),
        (
            """
Signature Packet
    Version: 4
    Type: SubkeyBinding
    Hash algo: SHA512
    Hashed area:
      Signature creation time: 2022-12-31 15:53:59 UTC
      Issuer: BBBBBB
    Unhashed area:
      Issuer: 42424242
""",
            "Unhashed area.Issuer",
            "42424242",
            does_not_raise(),
        ),
        (
            """
Signature Packet
    Version: 4
    Type: SubkeyBinding
    Hash algo: SHA512
    Hashed area:
      Signature creation time: 2022-12-31 15:53:59 UTC
    Unhashed area:
      Issuer: 42424242
""",
            "Hashed area|Unhashed area.Issuer",
            "42424242",
            does_not_raise(),
        ),
        (
            """
Signature Packet
    Version: 4
    Type: SubkeyBinding
    Hash algo: SHA1
    Hashed area:
      Signature creation time: 2022-12-31
""",
            "*.Signature creation time",
            "2022-12-31",
            does_not_raise(),
        ),
        (
            """
Signature Packet
    a:
      b:
        x: foo
    b:
      b:
        c: bar
""",
            "*.b.c",
            "bar",
            does_not_raise(),
        ),
        (
            """
Signature Packet
    a:
      b:
        x:
          y:
            z: foo
    b:
      b:
        x:
          y:
            z: foo
          w:
            w: foo
        k:
          i:
            c: bar
""",
            "*.b.*.*.c",
            "bar",
            does_not_raise(),
        ),
        (
            """
Signature Packet
    a:
      c:
        b: foo
    a:
      b: bar
""",
            "a.b",
            "bar",
            does_not_raise(),
        ),
        (
            """
Signature Packet
    Version: 4
    Type: SubkeyBinding
    Hash algo: SHA512
    Hashed area:
      Signature creation time: 2022-12-31 15:53:59 UTC
    Unhashed area:
      Issuer: 42424242
    Issuer: BBBBBBBB
""",
            "Hashed area.Issuer",
            None,
            raises(Exception),
        ),
    ],
)
@patch("libkeyringctl.sequoia.packet_dump")
def test_packet_dump_field(
    packet_dump_mock: Mock,
    packet_dump_return: str,
    query: str,
    result: str,
    expectation: ContextManager[str],
) -> None:
    packet_dump_mock.return_value = packet_dump_return

    with expectation:
        assert sequoia.packet_dump_field(packet=Path("packet"), query=query) == result


@patch("libkeyringctl.sequoia.packet_dump_field")
def test_packet_signature_creation_time(packet_dump_field_mock: Mock) -> None:
    creation_time = "2021-10-31 00:48:09 UTC"
    packet_dump_field_mock.return_value = creation_time
    assert sequoia.packet_signature_creation_time(packet=Path("packet")) == datetime.strptime(
        creation_time, "%Y-%m-%d %H:%M:%S %Z"
    )


@patch("libkeyringctl.sequoia.packet_dump")
def test_packet_kinds(packet_dump_mock: Mock) -> None:
    lines = [
        "Type1 something",
        " foo",
        "Type2",
        "WARNING",
        "Type3  other",
        " bar",
    ]
    path = Path("foo")
    packet_dump_mock.return_value = "\n".join(lines)

    assert sequoia.packet_kinds(packet=path) == [PacketKind("Type1"), PacketKind("Type2"), PacketKind("Type3")]


@patch("libkeyringctl.sequoia.packet_signature_creation_time")
def test_latest_certification(packet_signature_creation_time_mock: Mock) -> None:
    now = datetime.now(tz=timezone.utc)
    later = now + timedelta(days=1)
    early_cert = Path("cert1")
    later_cert = Path("cert2")

    packet_signature_creation_time_mock.side_effect = [now, later]
    assert sequoia.latest_certification(certifications=[early_cert, later_cert]) == later_cert

    packet_signature_creation_time_mock.side_effect = [later, now]
    assert sequoia.latest_certification(certifications=[later_cert, early_cert]) == later_cert


@mark.parametrize("output", [(None), (Path("output"))])
@patch("libkeyringctl.sequoia.system")
def test_key_extract_certificate(system_mock: Mock, output: Optional[Path]) -> None:
    system_mock.return_value = "return"
    assert sequoia.key_extract_certificate(key=Path("key"), output=output) == "return"
    name, args, kwargs = system_mock.mock_calls[0]
    if output:
        assert str(output) == args[0][-1]


@mark.parametrize("output", [(None), (Path("output"))])
@patch("libkeyringctl.sequoia.system")
def test_certify(system_mock: Mock, output: Optional[Path]) -> None:
    system_mock.return_value = "return"
    assert sequoia.certify(key=Path("key"), certificate=Path("cert"), uid=Uid("uid"), output=output) == "return"
    name, args, kwargs = system_mock.mock_calls[0]
    if output:
        assert str(output) == args[0][-1]
