"""
.. codeauthor:: Tsuyoshi Hombashi <example@nnthink.com>
"""

from datetime import datetime
from textwrap import dedent

import pytest
import pytz

from pingparsing import ParseError, PingResult

from .common import PingTestData, ping_parser  # noqa: W0611
from .data import (
    DEBIAN_SUCCESS_0,
    UBUNTU_SUCCESS_0,
    UBUNTU_SUCCESS_1,
    UBUNTU_SUCCESS_2,
    WINDOWS7SP1_SUCCESS,
)

PING_FEDORA_EMPTY_BODY = b"""\
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.

--- 192.168.0.1 ping statistics ---
"""
PING_WINDOWS_INVALID = b"""\
Pinging 192.168.207.100 with 32 bytes of data:
Request timed out.
Request timed out.
Request timed out.
Request timed out.

Ping statistics for 192.168.207.100:
"""

DEBIAN_UNREACHABLE_0 = PingTestData(
    dedent(
        """\
        PING 192.168.207.100 (192.168.207.100) 56(84) bytes of data.

        --- 192.168.207.100 ping statistics ---
        5 packets transmitted, 0 received, 100% packet loss, time 4009ms
        """
    ),
    {
        "destination": "192.168.207.100",
        "packet_transmit": 5,
        "packet_receive": 0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": None,
        "packet_loss_count": 5,
        "packet_loss_rate": 100.0,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)
DEBIAN_UNREACHABLE_1 = PingTestData(
    DEBIAN_UNREACHABLE_0.value + "\n", DEBIAN_UNREACHABLE_0.expected, []
)
DEBIAN_UNREACHABLE_2 = PingTestData(
    DEBIAN_UNREACHABLE_1.value + "\n", DEBIAN_UNREACHABLE_0.expected, []
)

UBUNTU_FAIL_0 = PingTestData(
    # ping -D -O <ip addr>
    dedent(
        """\
        PING 192.168.11.222 (192.168.11.222) 56(84) bytes of data.
        [1596881133.081556] no answer yet for icmp_seq=1
        [1596881133.081898] 64 bytes from 192.168.11.222: icmp_seq=2 ttl=64 time=0.262 ms
        [1596881135.129517] no answer yet for icmp_seq=3
        [1596881136.153055] no answer yet for icmp_seq=4
        [1596881137.180056] no answer yet for icmp_seq=5
        [1596881137.180326] 64 bytes from 192.168.11.222: icmp_seq=6 ttl=64 time=0.221 ms
        [1596881138.201538] 64 bytes from 192.168.11.222: icmp_seq=7 ttl=64 time=0.257 ms
        ^C
        --- 192.168.11.222 ping statistics ---
        8 packets transmitted, 3 received, 62.5% packet loss, time 154ms
        """
    ),
    {
        "destination": "192.168.11.222",
        "packet_transmit": 8,
        "packet_receive": 3,
        "packet_loss_count": 5,
        "packet_loss_rate": 62.5,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": 0.0,
    },
    [
        {
            "timestamp": datetime(2020, 8, 8, 10, 5, 33, 81556, tzinfo=pytz.UTC),
            "icmp_seq": 1,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.11.222",
            "timestamp": datetime(2020, 8, 8, 10, 5, 33, 81898, tzinfo=pytz.UTC),
            "icmp_seq": 2,
            "ttl": 64,
            "time": 0.262,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2020, 8, 8, 10, 5, 35, 129517, tzinfo=pytz.UTC),
            "icmp_seq": 3,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2020, 8, 8, 10, 5, 36, 153055, tzinfo=pytz.UTC),
            "icmp_seq": 4,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2020, 8, 8, 10, 5, 37, 180056, tzinfo=pytz.UTC),
            "icmp_seq": 5,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.11.222",
            "timestamp": datetime(2020, 8, 8, 10, 5, 37, 180326, tzinfo=pytz.UTC),
            "icmp_seq": 6,
            "ttl": 64,
            "time": 0.221,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.11.222",
            "timestamp": datetime(2020, 8, 8, 10, 5, 38, 201538, tzinfo=pytz.UTC),
            "icmp_seq": 7,
            "ttl": 64,
            "time": 0.257,
            "duplicate": False,
        },
    ],
)

FEDORA_DUP_LOSS = PingTestData(
    dedent(
        """\
        PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.

        --- 192.168.0.1 ping statistics ---
        1688 packets transmitted, 1553 received, +1 duplicates, 7% packet loss, time 2987ms
        rtt min/avg/max/mdev = 0.282/0.642/11.699/0.699 ms, pipe 2, ipg/ewma 1.770/0.782 ms
        """
    ),
    {
        "destination": "192.168.0.1",
        "packet_transmit": 1688,
        "packet_receive": 1553,
        "packet_duplicate_count": 1,
        "packet_duplicate_rate": 0.0643915003219575,
        "packet_loss_count": 135,
        "packet_loss_rate": 7.9976303317535535,
        "rtt_min": 0.282,
        "rtt_max": 11.699,
        "rtt_mdev": 0.699,
        "rtt_avg": 0.642,
    },
    [],
)
FEDORA_UNREACHABLE = PingTestData(
    dedent(
        """\
        PING 192.168.207.100 (192.168.207.100) 56(84) bytes of data.
        From 192.168.207.128 icmp_seq=1 Destination Host Unreachable
        From 192.168.207.128 icmp_seq=2 Destination Host Unreachable
        From 192.168.207.128 icmp_seq=3 Destination Host Unreachable
        From 192.168.207.128 icmp_seq=4 Destination Host Unreachable
        From 192.168.207.128 icmp_seq=5 Destination Host Unreachable

        --- 192.168.207.100 ping statistics ---
        5 packets transmitted, 0 received, +5 errors, 100% packet loss, time 4003ms
        """
    ),
    {
        "destination": "192.168.207.100",
        "packet_transmit": 5,
        "packet_receive": 0,
        "packet_loss_count": 5,
        "packet_loss_rate": 100.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": None,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)

MACOS_SUCCESS_0 = PingTestData(
    dedent(
        """\
        PING google.com (172.217.6.238): 56 data bytes
        64 bytes from 172.217.6.238: icmp_seq=0 ttl=53 time=20.482 ms
        64 bytes from 172.217.6.238: icmp_seq=1 ttl=53 time=32.550 ms
        64 bytes from 172.217.6.238: icmp_seq=2 ttl=53 time=32.013 ms
        64 bytes from 172.217.6.238: icmp_seq=3 ttl=53 time=28.498 ms
        64 bytes from 172.217.6.238: icmp_seq=4 ttl=53 time=46.093 ms

        --- google.com ping statistics ---
        5 packets transmitted, 5 packets received, 0.0% packet loss
        round-trip min/avg/max/stddev = 20.482/31.927/46.093/8.292 ms
        """
    ),
    {
        "destination": "google.com",
        "packet_transmit": 5,
        "packet_receive": 5,
        "packet_loss_count": 0,
        "packet_loss_rate": 0.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": 0,
        "rtt_min": 20.482,
        "rtt_avg": 31.927,
        "rtt_max": 46.093,
        "rtt_mdev": 8.292,
    },
    [
        {
            "bytes": 64,
            "destination": "172.217.6.238",
            "icmp_seq": 0,
            "ttl": 53,
            "time": 20.482,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "172.217.6.238",
            "icmp_seq": 1,
            "ttl": 53,
            "time": 32.55,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "172.217.6.238",
            "icmp_seq": 2,
            "ttl": 53,
            "time": 32.013,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "172.217.6.238",
            "icmp_seq": 3,
            "ttl": 53,
            "time": 28.498,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "172.217.6.238",
            "icmp_seq": 4,
            "ttl": 53,
            "time": 46.093,
            "duplicate": False,
        },
    ],
)
MACOS_SUCCESS_1 = PingTestData(
    dedent(
        """\
        PING github.com (192.30.255.113): 56 data bytes

        --- github.com ping statistics ---
        10 packets transmitted, 10 packets received, 0.0% packet loss
        round-trip min/avg/max/stddev = 218.391/283.477/405.879/70.170 ms
        """
    ),
    {
        "destination": "github.com",
        "packet_transmit": 10,
        "packet_receive": 10,
        "packet_loss_count": 0,
        "packet_loss_rate": 0.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": 0,
        "rtt_min": 218.391,
        "rtt_avg": 283.477,
        "rtt_max": 405.879,
        "rtt_mdev": 70.170,
    },
    [],
)
MACOS_UNREACHABLE_0 = PingTestData(
    dedent(
        """\
        PING twitter.com (59.24.3.173): 56 data bytes
        ^C
        --- twitter.com ping statistics ---
        59 packets transmitted, 0 packets received, 100.0% packet loss
        """
    ),
    {
        "destination": "twitter.com",
        "packet_transmit": 59,
        "packet_receive": 0,
        "packet_loss_rate": 100.0,
        "packet_loss_count": 59,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
        "packet_duplicate_rate": None,
        "packet_duplicate_count": 0,
    },
    [],
)
MACOS_UNREACHABLE_1 = PingTestData(
    dedent(
        """\
        PING twitter.com (31.13.78.66): 56 data bytes

        --- twitter.com ping statistics ---
        10 packets transmitted, 0 packets received, 100.0% packet loss
        """
    ),
    {
        "destination": "twitter.com",
        "packet_transmit": 10,
        "packet_receive": 0,
        "packet_loss_count": 10,
        "packet_loss_rate": 100.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": None,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)
MACOS_UNREACHABLE_2 = PingTestData(
    dedent(
        """\
        PING example.net (93.184.216.34): 56 data bytes

        --- example.net ping statistics ---
        10 packets transmitted, 0 packets received, 100.0% packet loss
        """
    ),
    {
        "destination": "example.net",
        "packet_transmit": 10,
        "packet_receive": 0,
        "packet_loss_count": 10,
        "packet_loss_rate": 100.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": None,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)
MACOS_DUPLICATE_0 = PingTestData(
    dedent(
        """\
        PING duplicate.com (31.13.78.66): 56 data bytes

        --- duplicate.com ping statistics ---
        3 packets transmitted, 3 packets received, +3 duplicates, 0% packet loss

        round-trip min/avg/max/stddev = 0.311/1.091/2.186/0.662 ms
        """
    ),
    {
        "destination": "duplicate.com",
        "packet_transmit": 3,
        "packet_receive": 3,
        "packet_loss_count": 0,
        "packet_loss_rate": 0.0,
        "packet_duplicate_count": 3,
        "packet_duplicate_rate": 100,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)

ALPINE_LINUX_SUCCESS = PingTestData(
    dedent(
        """\
        PING heise.de (193.99.144.80): 56 data bytes

        --- heise.de ping statistics ---
        5 packets transmitted, 5 packets received, 0% packet loss
        round-trip min/avg/max = 0.638/0.683/0.746 ms
        """
    ),
    {
        "destination": "heise.de",
        "packet_transmit": 5,
        "packet_receive": 5,
        "packet_loss_count": 0,
        "packet_loss_rate": 0.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": 0,
        "rtt_min": 0.638,
        "rtt_avg": 0.683,
        "rtt_max": 0.746,
        "rtt_mdev": None,
    },
    [],
)
ALPINE_LINUX_DUP_LOSS = PingTestData(
    dedent(
        """\
        PING 192.168.2.106 (192.168.2.106): 56 data bytes
        64 bytes from 192.168.2.106: seq=0 ttl=64 time=0.936 ms
        64 bytes from 192.168.2.106: seq=0 ttl=64 time=1.003 ms (DUP!)
        64 bytes from 192.168.2.106: seq=1 ttl=64 time=0.802 ms
        64 bytes from 192.168.2.106: seq=2 ttl=64 time=0.696 ms
        64 bytes from 192.168.2.106: seq=3 ttl=64 time=0.664 ms
        64 bytes from 192.168.2.106: seq=4 ttl=64 time=1.194 ms
        64 bytes from 192.168.2.106: seq=5 ttl=64 time=0.613 ms
        64 bytes from 192.168.2.106: seq=6 ttl=64 time=0.898 ms
        64 bytes from 192.168.2.106: seq=8 ttl=64 time=1.066 ms
        64 bytes from 192.168.2.106: seq=9 ttl=64 time=1.144 ms
        64 bytes from 192.168.2.106: seq=9 ttl=64 time=1.219 ms (DUP!)

        --- 192.168.2.106 ping statistics ---
        10 packets transmitted, 9 packets received, 2 duplicates, 10% packet loss
        round-trip min/avg/max = 0.613/0.930/1.219 ms
        """
    ),
    {
        "destination": "192.168.2.106",
        "packet_transmit": 10,
        "packet_receive": 9,
        "packet_duplicate_count": 2,
        "packet_duplicate_rate": 22.22222222222222,
        "packet_loss_count": 1,
        "packet_loss_rate": 10.0,
        "rtt_min": 0.613,
        "rtt_avg": 0.93,
        "rtt_max": 1.219,
        "rtt_mdev": None,
    },
    [
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 0,
            "ttl": 64,
            "time": 0.936,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 0,
            "ttl": 64,
            "time": 1.003,
            "duplicate": True,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 1,
            "ttl": 64,
            "time": 0.802,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 2,
            "ttl": 64,
            "time": 0.696,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 3,
            "ttl": 64,
            "time": 0.664,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 4,
            "ttl": 64,
            "time": 1.194,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 5,
            "ttl": 64,
            "time": 0.613,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 6,
            "ttl": 64,
            "time": 0.898,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 8,
            "ttl": 64,
            "time": 1.066,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 9,
            "ttl": 64,
            "time": 1.144,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 9,
            "ttl": 64,
            "time": 1.219,
            "duplicate": True,
        },
    ],
)

IPV6_LINUX = PingTestData(
    dedent(
        r"""\
        PING ff02::2%usb0(ff02::2%usb0) 56 data bytes
        64 bytes from fe80::783c:caff:fe12:b46c%usb0: icmp_seq=1 ttl=64 time=2.71 ms

        --- ff02::2%usb0 ping statistics ---
        1 packets transmitted, 1 received, 0% packet loss, time 0ms
        rtt min/avg/max/mdev = 2.708/2.708/2.708/0.000 ms
        """
    ),
    {
        "destination": r"ff02::2%usb0",
        "packet_transmit": 1,
        "packet_receive": 1,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": 0,
        "packet_loss_count": 0,
        "packet_loss_rate": 0.0,
        "rtt_min": 2.708,
        "rtt_avg": 2.708,
        "rtt_max": 2.708,
        "rtt_mdev": 0.0,
    },
    [
        {
            "bytes": 64,
            "destination": r"fe80::783c:caff:fe12:b46c%usb0",
            "icmp_seq": 1,
            "ttl": 64,
            "time": 2.71,
            "duplicate": False,
        },
    ],
)

LINUX_PIPE = PingTestData(
    dedent(
        r"""\
        PING 91.221.122.179 (91.221.122.179) 64(92) bytes of data.
        [1622145167.999326] no answer yet for icmp_seq=1
        [1622145168.201746] no answer yet for icmp_seq=2
        [1622145168.405761] no answer yet for icmp_seq=3
        [1622145168.609750] no answer yet for icmp_seq=4
        [1622145170.826836] From 91.221.122.179 icmp_seq=1 Destination Host Unreachable
        [1622145170.826888] From 91.221.122.179 icmp_seq=2 Destination Host Unreachable
        [1622145170.826896] From 91.221.122.179 icmp_seq=3 Destination Host Unreachable
        [1622145170.826901] From 91.221.122.179 icmp_seq=4 Destination Host Unreachable
        [1622145170.826907] From 91.221.122.179 icmp_seq=5 Destination Host Unreachable

        --- 91.221.122.179 ping statistics ---
        5 packets transmitted, 0 received, +5 errors, 100% packet loss, time 811ms
        pipe 5
        """
    ),
    {
        "destination": "91.221.122.179",
        "packet_transmit": 5,
        "packet_receive": 0,
        "packet_loss_count": 5,
        "packet_loss_rate": 100.0,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": None,
    },
    [
        {
            "timestamp": datetime(2021, 5, 27, 19, 52, 47, 999326, tzinfo=pytz.UTC),
            "icmp_seq": 1,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2021, 5, 27, 19, 52, 48, 201746, tzinfo=pytz.UTC),
            "icmp_seq": 2,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2021, 5, 27, 19, 52, 48, 405761, tzinfo=pytz.UTC),
            "icmp_seq": 3,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2021, 5, 27, 19, 52, 48, 609750, tzinfo=pytz.UTC),
            "icmp_seq": 4,
            "duplicate": False,
        },
    ],
)

WINDOWS10_LOSS = PingTestData(
    dedent(
        """\
        Pinging 192.168.2.106 with 32 bytes of data:
        Reply from 192.168.2.106: bytes=32 time=16ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=6ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=12ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=16ms TTL=64
        Request timed out.
        Reply from 192.168.2.106: bytes=32 time=8ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=33ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=13ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=23ms TTL=64
        Reply from 192.168.2.106: bytes=32 time<1ms TTL=64

        Ping statistics for 192.168.2.106:
            Packets: Sent = 10, Received = 9, Lost = 1 (10% los
        Approximate round trip times in milli-seconds:
            Minimum = 0ms, Maximum = 33ms, Average = 14ms
        """
    ),
    {
        "destination": "192.168.2.106",
        "packet_transmit": 10,
        "packet_receive": 9,
        "packet_loss_count": 1,
        "packet_loss_rate": 10.0,
        "packet_duplicate_count": None,
        "packet_duplicate_rate": None,
        "rtt_min": 0.0,
        "rtt_avg": 14.0,
        "rtt_max": 33.0,
        "rtt_mdev": None,
    },
    [
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 16.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 6.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 12.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 16.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 8.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 33.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 13.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 23.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 1.0, "duplicate": False},
    ],
)
WINDOWS_UNREACHABLE_0 = PingTestData(
    dedent(
        """\
        Pinging 192.168.207.100 with 32 bytes of data:
        Request timed out.
        Request timed out.
        Request timed out.
        Request timed out.

        Ping statistics for 192.168.207.100:
            Packets: Sent = 4, Received = 0, Lost = 4 (100% loss),
        """
    ),
    {
        "destination": "192.168.207.100",
        "packet_transmit": 4,
        "packet_receive": 0,
        "packet_loss_count": 4,
        "packet_loss_rate": 100.0,
        "packet_duplicate_count": None,
        "packet_duplicate_rate": None,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)
WINDOWS_UNREACHABLE_1 = PingTestData(
    WINDOWS_UNREACHABLE_0.value + "\n", WINDOWS_UNREACHABLE_0.expected, []
)
WINDOWS_UNREACHABLE_2 = PingTestData(
    WINDOWS_UNREACHABLE_1.value + "\n", WINDOWS_UNREACHABLE_0.expected, []
)

class Test_PingParsing_parse:
    @pytest.mark.parametrize(
        ["test_data", "parser_name"],
        [
            [DEBIAN_SUCCESS_0, "Linux"],
            [DEBIAN_UNREACHABLE_0, "Linux"],
            [DEBIAN_UNREACHABLE_1, "Linux"],
            [DEBIAN_UNREACHABLE_2, "Linux"],
            [UBUNTU_SUCCESS_0, "Linux"],
            [UBUNTU_FAIL_0, "Linux"],
            [FEDORA_DUP_LOSS, "Linux"],
            [FEDORA_UNREACHABLE, "Linux"],
            [MACOS_SUCCESS_0, "macOS"],
            [MACOS_SUCCESS_1, "macOS"],
            [MACOS_UNREACHABLE_0, "macOS"],
            [MACOS_UNREACHABLE_1, "macOS"],
            [MACOS_UNREACHABLE_2, "macOS"],
            [MACOS_DUPLICATE_0, "macOS"],
            [ALPINE_LINUX_SUCCESS, "AlpineLinux"],
            [ALPINE_LINUX_DUP_LOSS, "AlpineLinux"],
            [WINDOWS7SP1_SUCCESS, "Windows"],
            [WINDOWS10_LOSS, "Windows"],
            [WINDOWS_UNREACHABLE_0, "Windows"],
            [WINDOWS_UNREACHABLE_1, "Windows"],
            [WINDOWS_UNREACHABLE_2, "Windows"],
            [IPV6_LINUX, "Linux"],
            [LINUX_PIPE, "Linux"],
        ],
    )
    def test_normal_text(self, ping_parser, test_data, parser_name):
        stats = ping_parser.parse(test_data.value)

        print(f"[input text]\n{test_data.value}\n")
        print(f"[expected]\n{test_data.expected}\n")
        print(f"[actual]\n{stats.as

        for icmp_reply in stats.icmp_replies:
            print(icmp_reply)

        assert ping_parser.parser_name == parser_name
        assert stats.as_dict() == test_data.expected
        assert stats.icmp_replies == test_data.replies

    @pytest.mark.xfail(run=False)
    @pytest.mark.parametrize(
        ["test_data", "parser_name"], [[UBUNTU_SUCCESS_1, "Linux"], [UBUNTU_SUCCESS_2, "Linux"]]
    )
    def test_normal_timestamp(self, ping_parser, test_data, parser_name):
        stats = ping_parser.parse(test_data.value)

        print(f"[input text]\n{test_data.value}\n")
        print(f"[expected]\n{test_data.expected}\n")
        print(f"[actual]\n{stats.as_dict()}\n")
        for icmp_reply in stats.icmp_replies:
            print(icmp_reply)

        assert ping_parser.parser_name == parser_name
        assert stats.as_dict() == test_data.expected
        assert stats.icmp_replies == test_data.replies

    def test_empty(self, ping_parser):
        ping_parser.parse(
            dedent(
                """\
            PING google.com (216.58.196.238) 56(84) bytes of data.

            --- google.com ping statistics ---
            60 packets transmitted, 60 received, 0% packet loss, time 59153ms
            rtt min/avg/max/mdev = 61.425/99.731/212.597/27.566 ms
            """
            )
        )
        stats = ping_parser.parse("")

        assert stats.is_empty()

    @pytest.mark.parametrize(
        ["value"],
        [
            [
                PingResult(
                    stdout="",
                    stderr="ping: unknown: Temporary failure in name resolution\n",
                    returncode=2,
                )
            ]
        ],
    )
    def test_ping_failure(self, ping_parser, value):
        stats = ping_parser.parse(value)
        assert stats.is_empty()

    @pytest.mark.parametrize(
        ["value", "expected"],
        [[PING_FEDORA_EMPTY_BODY, ParseError], [PING_WINDOWS_INVALID, ParseError]],
    )
    def test_exception(self, ping_parser, value, expected):
        with pytest.raises(expected):
            ping_parser.parse(value)

class Test_PingParsing_as_tuple:
    def test_normal(self, ping_parser):
        stats = ping_parser.parse(DEBIAN_SUCCESS_0.value)
        result = stats.as_tuple()

        assert result.destination == "google.com"
        assert result.packet_transmit == 60
        assert result.packet_receive == 60
        assert result.packet_loss_count == 0
        assert result.packet_loss_rate == 0.0
        assert result.packet_duplicate_rate == 0
        assert result.packet_duplicate_count == 0
        assert result.rtt_min == 61.425
        assert result.rtt_avg == 99.731
        assert result.rtt_max == 212.597
        assert result.rtt_mdev == 27.566
        assert stats.icmp_replies == []


"""
.. codeauthor:: Tsuyoshi Hombashi <example@nnthink.com>
"""

from datetime import datetime
from textwrap import dedent

import pytest
import pytz

from pingparsing import ParseError, PingResult

from .common import PingTestData, ping_parser  # noqa: W0611
from .data import (
    DEBIAN_SUCCESS_0,
    UBUNTU_SUCCESS_0,
    UBUNTU_SUCCESS_1,
    UBUNTU_SUCCESS_2,
    WINDOWS7SP1_SUCCESS,
)

PING_FEDORA_EMPTY_BODY = b"""\
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.

--- 192.168.0.1 ping statistics ---
"""
PING_WINDOWS_INVALID = b"""\
Pinging 192.168.207.100 with 32 bytes of data:
Request timed out.
Request timed out.
Request timed out.
Request timed out.

Ping statistics for 192.168.207.100:
"""

DEBIAN_UNREACHABLE_0 = PingTestData(
    dedent(
        """\
        PING 192.168.207.100 (192.168.207.100) 56(84) bytes of data.

        --- 192.168.207.100 ping statistics ---
        5 packets transmitted, 0 received, 100% packet loss, time 4009ms
        """
    ),
    {
        "destination": "192.168.207.100",
        "packet_transmit": 5,
        "packet_receive": 0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": None,
        "packet_loss_count": 5,
        "packet_loss_rate": 100.0,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)
DEBIAN_UNREACHABLE_1 = PingTestData(
    DEBIAN_UNREACHABLE_0.value + "\n", DEBIAN_UNREACHABLE_0.expected, []
)
DEBIAN_UNREACHABLE_2 = PingTestData(
    DEBIAN_UNREACHABLE_1.value + "\n", DEBIAN_UNREACHABLE_0.expected, []
)

UBUNTU_FAIL_0 = PingTestData(
    # ping -D -O <ip addr>
    dedent(
        """\
        PING 192.168.11.222 (192.168.11.222) 56(84) bytes of data.
        [1596881133.081556] no answer yet for icmp_seq=1
        [1596881133.081898] 64 bytes from 192.168.11.222: icmp_seq=2 ttl=64 time=0.262 ms
        [1596881135.129517] no answer yet for icmp_seq=3
        [1596881136.153055] no answer yet for icmp_seq=4
        [1596881137.180056] no answer yet for icmp_seq=5
        [1596881137.180326] 64 bytes from 192.168.11.222: icmp_seq=6 ttl=64 time=0.221 ms
        [1596881138.201538] 64 bytes from 192.168.11.222: icmp_seq=7 ttl=64 time=0.257 ms
        ^C
        --- 192.168.11.222 ping statistics ---
        8 packets transmitted, 3 received, 62.5% packet loss, time 154ms
        """
    ),
    {
        "destination": "192.168.11.222",
        "packet_transmit": 8,
        "packet_receive": 3,
        "packet_loss_count": 5,
        "packet_loss_rate": 62.5,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": 0.0,
    },
    [
        {
            "timestamp": datetime(2020, 8, 8, 10, 5, 33, 81556, tzinfo=pytz.UTC),
            "icmp_seq": 1,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.11.222",
            "timestamp": datetime(2020, 8, 8, 10, 5, 33, 81898, tzinfo=pytz.UTC),
            "icmp_seq": 2,
            "ttl": 64,
            "time": 0.262,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2020, 8, 8, 10, 5, 35, 129517, tzinfo=pytz.UTC),
            "icmp_seq": 3,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2020, 8, 8, 10, 5, 36, 153055, tzinfo=pytz.UTC),
            "icmp_seq": 4,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2020, 8, 8, 10, 5, 37, 180056, tzinfo=pytz.UTC),
            "icmp_seq": 5,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.11.222",
            "timestamp": datetime(2020, 8, 8, 10, 5, 37, 180326, tzinfo=pytz.UTC),
            "icmp_seq": 6,
            "ttl": 64,
            "time": 0.221,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.11.222",
            "timestamp": datetime(2020, 8, 8, 10, 5, 38, 201538, tzinfo=pytz.UTC),
            "icmp_seq": 7,
            "ttl": 64,
            "time": 0.257,
            "duplicate": False,
        },
    ],
)

FEDORA_DUP_LOSS = PingTestData(
    dedent(
        """\
        PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.

        --- 192.168.0.1 ping statistics ---
        1688 packets transmitted, 1553 received, +1 duplicates, 7% packet loss, time 2987ms
        rtt min/avg/max/mdev = 0.282/0.642/11.699/0.699 ms, pipe 2, ipg/ewma 1.770/0.782 ms
        """
    ),
    {
        "destination": "192.168.0.1",
        "packet_transmit": 1688,
        "packet_receive": 1553,
        "packet_duplicate_count": 1,
        "packet_duplicate_rate": 0.0643915003219575,
        "packet_loss_count": 135,
        "packet_loss_rate": 7.9976303317535535,
        "rtt_min": 0.282,
        "rtt_max": 11.699,
        "rtt_mdev": 0.699,
        "rtt_avg": 0.642,
    },
    [],
)
FEDORA_UNREACHABLE = PingTestData(
    dedent(
        """\
        PING 192.168.207.100 (192.168.207.100) 56(84) bytes of data.
        From 192.168.207.128 icmp_seq=1 Destination Host Unreachable
        From 192.168.207.128 icmp_seq=2 Destination Host Unreachable
        From 192.168.207.128 icmp_seq=3 Destination Host Unreachable
        From 192.168.207.128 icmp_seq=4 Destination Host Unreachable
        From 192.168.207.128 icmp_seq=5 Destination Host Unreachable

        --- 192.168.207.100 ping statistics ---
        5 packets transmitted, 0 received, +5 errors, 100% packet loss, time 4003ms
        """
    ),
    {
        "destination": "192.168.207.100",
        "packet_transmit": 5,
        "packet_receive": 0,
        "packet_loss_count": 5,
        "packet_loss_rate": 100.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": None,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)

MACOS_SUCCESS_0 = PingTestData(
    dedent(
        """\
        PING google.com (172.217.6.238): 56 data bytes
        64 bytes from 172.217.6.238: icmp_seq=0 ttl=53 time=20.482 ms
        64 bytes from 172.217.6.238: icmp_seq=1 ttl=53 time=32.550 ms
        64 bytes from 172.217.6.238: icmp_seq=2 ttl=53 time=32.013 ms
        64 bytes from 172.217.6.238: icmp_seq=3 ttl=53 time=28.498 ms
        64 bytes from 172.217.6.238: icmp_seq=4 ttl=53 time=46.093 ms

        --- google.com ping statistics ---
        5 packets transmitted, 5 packets received, 0.0% packet loss
        round-trip min/avg/max/stddev = 20.482/31.927/46.093/8.292 ms
        """
    ),
    {
        "destination": "google.com",
        "packet_transmit": 5,
        "packet_receive": 5,
        "packet_loss_count": 0,
        "packet_loss_rate": 0.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": 0,
        "rtt_min": 20.482,
        "rtt_avg": 31.927,
        "rtt_max": 46.093,
        "rtt_mdev": 8.292,
    },
    [
        {
            "bytes": 64,
            "destination": "172.217.6.238",
            "icmp_seq": 0,
            "ttl": 53,
            "time": 20.482,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "172.217.6.238",
            "icmp_seq": 1,
            "ttl": 53,
            "time": 32.55,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "172.217.6.238",
            "icmp_seq": 2,
            "ttl": 53,
            "time": 32.013,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "172.217.6.238",
            "icmp_seq": 3,
            "ttl": 53,
            "time": 28.498,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "172.217.6.238",
            "icmp_seq": 4,
            "ttl": 53,
            "time": 46.093,
            "duplicate": False,
        },
    ],
)
MACOS_SUCCESS_1 = PingTestData(
    dedent(
        """\
        PING github.com (192.30.255.113): 56 data bytes

        --- github.com ping statistics ---
        10 packets transmitted, 10 packets received, 0.0% packet loss
        round-trip min/avg/max/stddev = 218.391/283.477/405.879/70.170 ms
        """
    ),
    {
        "destination": "github.com",
        "packet_transmit": 10,
        "packet_receive": 10,
        "packet_loss_count": 0,
        "packet_loss_rate": 0.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": 0,
        "rtt_min": 218.391,
        "rtt_avg": 283.477,
        "rtt_max": 405.879,
        "rtt_mdev": 70.170,
    },
    [],
)
MACOS_UNREACHABLE_0 = PingTestData(
    dedent(
        """\
        PING twitter.com (59.24.3.173): 56 data bytes
        ^C
        --- twitter.com ping statistics ---
        59 packets transmitted, 0 packets received, 100.0% packet loss
        """
    ),
    {
        "destination": "twitter.com",
        "packet_transmit": 59,
        "packet_receive": 0,
        "packet_loss_rate": 100.0,
        "packet_loss_count": 59,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
        "packet_duplicate_rate": None,
        "packet_duplicate_count": 0,
    },
    [],
)
MACOS_UNREACHABLE_1 = PingTestData(
    dedent(
        """\
        PING twitter.com (31.13.78.66): 56 data bytes

        --- twitter.com ping statistics ---
        10 packets transmitted, 0 packets received, 100.0% packet loss
        """
    ),
    {
        "destination": "twitter.com",
        "packet_transmit": 10,
        "packet_receive": 0,
        "packet_loss_count": 10,
        "packet_loss_rate": 100.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": None,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)
MACOS_UNREACHABLE_2 = PingTestData(
    dedent(
        """\
        PING example.net (93.184.216.34): 56 data bytes

        --- example.net ping statistics ---
        10 packets transmitted, 0 packets received, 100.0% packet loss
        """
    ),
    {
        "destination": "example.net",
        "packet_transmit": 10,
        "packet_receive": 0,
        "packet_loss_count": 10,
        "packet_loss_rate": 100.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": None,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)
MACOS_DUPLICATE_0 = PingTestData(
    dedent(
        """\
        PING duplicate.com (31.13.78.66): 56 data bytes

        --- duplicate.com ping statistics ---
        3 packets transmitted, 3 packets received, +3 duplicates, 0% packet loss

        round-trip min/avg/max/stddev = 0.311/1.091/2.186/0.662 ms
        """
    ),
    {
        "destination": "duplicate.com",
        "packet_transmit": 3,
        "packet_receive": 3,
        "packet_loss_count": 0,
        "packet_loss_rate": 0.0,
        "packet_duplicate_count": 3,
        "packet_duplicate_rate": 100,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)

ALPINE_LINUX_SUCCESS = PingTestData(
    dedent(
        """\
        PING heise.de (193.99.144.80): 56 data bytes

        --- heise.de ping statistics ---
        5 packets transmitted, 5 packets received, 0% packet loss
        round-trip min/avg/max = 0.638/0.683/0.746 ms
        """
    ),
    {
        "destination": "heise.de",
        "packet_transmit": 5,
        "packet_receive": 5,
        "packet_loss_count": 0,
        "packet_loss_rate": 0.0,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": 0,
        "rtt_min": 0.638,
        "rtt_avg": 0.683,
        "rtt_max": 0.746,
        "rtt_mdev": None,
    },
    [],
)
ALPINE_LINUX_DUP_LOSS = PingTestData(
    dedent(
        """\
        PING 192.168.2.106 (192.168.2.106): 56 data bytes
        64 bytes from 192.168.2.106: seq=0 ttl=64 time=0.936 ms
        64 bytes from 192.168.2.106: seq=0 ttl=64 time=1.003 ms (DUP!)
        64 bytes from 192.168.2.106: seq=1 ttl=64 time=0.802 ms
        64 bytes from 192.168.2.106: seq=2 ttl=64 time=0.696 ms
        64 bytes from 192.168.2.106: seq=3 ttl=64 time=0.664 ms
        64 bytes from 192.168.2.106: seq=4 ttl=64 time=1.194 ms
        64 bytes from 192.168.2.106: seq=5 ttl=64 time=0.613 ms
        64 bytes from 192.168.2.106: seq=6 ttl=64 time=0.898 ms
        64 bytes from 192.168.2.106: seq=8 ttl=64 time=1.066 ms
        64 bytes from 192.168.2.106: seq=9 ttl=64 time=1.144 ms
        64 bytes from 192.168.2.106: seq=9 ttl=64 time=1.219 ms (DUP!)

        --- 192.168.2.106 ping statistics ---
        10 packets transmitted, 9 packets received, 2 duplicates, 10% packet loss
        round-trip min/avg/max = 0.613/0.930/1.219 ms
        """
    ),
    {
        "destination": "192.168.2.106",
        "packet_transmit": 10,
        "packet_receive": 9,
        "packet_duplicate_count": 2,
        "packet_duplicate_rate": 22.22222222222222,
        "packet_loss_count": 1,
        "packet_loss_rate": 10.0,
        "rtt_min": 0.613,
        "rtt_avg": 0.93,
        "rtt_max": 1.219,
        "rtt_mdev": None,
    },
    [
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 0,
            "ttl": 64,
            "time": 0.936,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 0,
            "ttl": 64,
            "time": 1.003,
            "duplicate": True,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 1,
            "ttl": 64,
            "time": 0.802,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 2,
            "ttl": 64,
            "time": 0.696,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 3,
            "ttl": 64,
            "time": 0.664,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 4,
            "ttl": 64,
            "time": 1.194,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 5,
            "ttl": 64,
            "time": 0.613,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 6,
            "ttl": 64,
            "time": 0.898,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 8,
            "ttl": 64,
            "time": 1.066,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 9,
            "ttl": 64,
            "time": 1.144,
            "duplicate": False,
        },
        {
            "bytes": 64,
            "destination": "192.168.2.106",
            "icmp_seq": 9,
            "ttl": 64,
            "time": 1.219,
            "duplicate": True,
        },
    ],
)

IPV6_LINUX = PingTestData(
    dedent(
        r"""\
        PING ff02::2%usb0(ff02::2%usb0) 56 data bytes
        64 bytes from fe80::783c:caff:fe12:b46c%usb0: icmp_seq=1 ttl=64 time=2.71 ms

        --- ff02::2%usb0 ping statistics ---
        1 packets transmitted, 1 received, 0% packet loss, time 0ms
        rtt min/avg/max/mdev = 2.708/2.708/2.708/0.000 ms
        """
    ),
    {
        "destination": r"ff02::2%usb0",
        "packet_transmit": 1,
        "packet_receive": 1,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": 0,
        "packet_loss_count": 0,
        "packet_loss_rate": 0.0,
        "rtt_min": 2.708,
        "rtt_avg": 2.708,
        "rtt_max": 2.708,
        "rtt_mdev": 0.0,
    },
    [
        {
            "bytes": 64,
            "destination": r"fe80::783c:caff:fe12:b46c%usb0",
            "icmp_seq": 1,
            "ttl": 64,
            "time": 2.71,
            "duplicate": False,
        },
    ],
)

LINUX_PIPE = PingTestData(
    dedent(
        r"""\
        PING 91.221.122.179 (91.221.122.179) 64(92) bytes of data.
        [1622145167.999326] no answer yet for icmp_seq=1
        [1622145168.201746] no answer yet for icmp_seq=2
        [1622145168.405761] no answer yet for icmp_seq=3
        [1622145168.609750] no answer yet for icmp_seq=4
        [1622145170.826836] From 91.221.122.179 icmp_seq=1 Destination Host Unreachable
        [1622145170.826888] From 91.221.122.179 icmp_seq=2 Destination Host Unreachable
        [1622145170.826896] From 91.221.122.179 icmp_seq=3 Destination Host Unreachable
        [1622145170.826901] From 91.221.122.179 icmp_seq=4 Destination Host Unreachable
        [1622145170.826907] From 91.221.122.179 icmp_seq=5 Destination Host Unreachable

        --- 91.221.122.179 ping statistics ---
        5 packets transmitted, 0 received, +5 errors, 100% packet loss, time 811ms
        pipe 5
        """
    ),
    {
        "destination": "91.221.122.179",
        "packet_transmit": 5,
        "packet_receive": 0,
        "packet_loss_count": 5,
        "packet_loss_rate": 100.0,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
        "packet_duplicate_count": 0,
        "packet_duplicate_rate": None,
    },
    [
        {
            "timestamp": datetime(2021, 5, 27, 19, 52, 47, 999326, tzinfo=pytz.UTC),
            "icmp_seq": 1,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2021, 5, 27, 19, 52, 48, 201746, tzinfo=pytz.UTC),
            "icmp_seq": 2,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2021, 5, 27, 19, 52, 48, 405761, tzinfo=pytz.UTC),
            "icmp_seq": 3,
            "duplicate": False,
        },
        {
            "timestamp": datetime(2021, 5, 27, 19, 52, 48, 609750, tzinfo=pytz.UTC),
            "icmp_seq": 4,
            "duplicate": False,
        },
    ],
)

WINDOWS10_LOSS = PingTestData(
    dedent(
        """\
        Pinging 192.168.2.106 with 32 bytes of data:
        Reply from 192.168.2.106: bytes=32 time=16ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=6ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=12ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=16ms TTL=64
        Request timed out.
        Reply from 192.168.2.106: bytes=32 time=8ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=33ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=13ms TTL=64
        Reply from 192.168.2.106: bytes=32 time=23ms TTL=64
        Reply from 192.168.2.106: bytes=32 time<1ms TTL=64

        Ping statistics for 192.168.2.106:
            Packets: Sent = 10, Received = 9, Lost = 1 (10% los
        Approximate round trip times in milli-seconds:
            Minimum = 0ms, Maximum = 33ms, Average = 14ms
        """
    ),
    {
        "destination": "192.168.2.106",
        "packet_transmit": 10,
        "packet_receive": 9,
        "packet_loss_count": 1,
        "packet_loss_rate": 10.0,
        "packet_duplicate_count": None,
        "packet_duplicate_rate": None,
        "rtt_min": 0.0,
        "rtt_avg": 14.0,
        "rtt_max": 33.0,
        "rtt_mdev": None,
    },
    [
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 16.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 6.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 12.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 16.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 8.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 33.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 13.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 23.0, "duplicate": False},
        {"bytes": 32, "destination": "192.168.2.106", "ttl": 64, "time": 1.0, "duplicate": False},
    ],
)
WINDOWS_UNREACHABLE_0 = PingTestData(
    dedent(
        """\
        Pinging 192.168.207.100 with 32 bytes of data:
        Request timed out.
        Request timed out.
        Request timed out.
        Request timed out.

        Ping statistics for 192.168.207.100:
            Packets: Sent = 4, Received = 0, Lost = 4 (100% loss),
        """
    ),
    {
        "destination": "192.168.207.100",
        "packet_transmit": 4,
        "packet_receive": 0,
        "packet_loss_count": 4,
        "packet_loss_rate": 100.0,
        "packet_duplicate_count": None,
        "packet_duplicate_rate": None,
        "rtt_min": None,
        "rtt_avg": None,
        "rtt_max": None,
        "rtt_mdev": None,
    },
    [],
)
WINDOWS_UNREACHABLE_1 = PingTestData(
    WINDOWS_UNREACHABLE_0.value + "\n", WINDOWS_UNREACHABLE_0.expected, []
)
WINDOWS_UNREACHABLE_2 = PingTestData(
    WINDOWS_UNREACHABLE_1.value + "\n", WINDOWS_UNREACHABLE_0.expected, []
)

class Test_PingParsing_parse:
    @pytest.mark.parametrize(
        ["test_data", "parser_name"],
        [
            [DEBIAN_SUCCESS_0, "Linux"],
            [DEBIAN_UNREACHABLE_0, "Linux"],
            [DEBIAN_UNREACHABLE_1, "Linux"],
            [DEBIAN_UNREACHABLE_2, "Linux"],
            [UBUNTU_SUCCESS_0, "Linux"],
            [UBUNTU_FAIL_0, "Linux"],
            [FEDORA_DUP_LOSS, "Linux"],
            [FEDORA_UNREACHABLE, "Linux"],
            [MACOS_SUCCESS_0, "macOS"],
            [MACOS_SUCCESS_1, "macOS"],
            [MACOS_UNREACHABLE_0, "macOS"],
            [MACOS_UNREACHABLE_1, "macOS"],
            [MACOS_UNREACHABLE_2, "macOS"],
            [MACOS_DUPLICATE_0, "macOS"],
            [ALPINE_LINUX_SUCCESS, "AlpineLinux"],
            [ALPINE_LINUX_DUP_LOSS, "AlpineLinux"],
            [WINDOWS7SP1_SUCCESS, "Windows"],
            [WINDOWS10_LOSS, "Windows"],
            [WINDOWS_UNREACHABLE_0, "Windows"],
            [WINDOWS_UNREACHABLE_1, "Windows"],
            [WINDOWS_UNREACHABLE_2, "Windows"],
            [IPV6_LINUX, "Linux"],
            [LINUX_PIPE, "Linux"],
        ],
    )
    def test_normal_text(self, ping_parser, test_data, parser_name):
        stats = ping_parser.parse(test_data.value)

        print(f"[input text]\n{test_data.value}\n")
        print(f"[expected]\n{test_data.expected}\n")
        print(f"[actual]\n{stats.as_dict()}\n")
        for icmp_reply in stats.icmp_replies:
            print(icmp_reply)

        assert ping_parser.parser_name == parser_name
        assert stats.as_dict() == test_data.expected
        assert stats.icmp_replies == test_data.replies

    @pytest.mark.xfail(run=False)
    @pytest.mark.parametrize(
        ["test_data", "parser_name"], [[UBUNTU_SUCCESS_1, "Linux"], [UBUNTU_SUCCESS_2, "Linux"]]
    )
    def test_normal_timestamp(self, ping_parser, test_data, parser_name):
        stats = ping_parser.parse(test_data.value)

        print(f"[input text]\n{test_data.value}\n")
        print(f"[expected]\n{test_data.expected}\n")
        print(f"[actual]\n{stats.as_dict()}\n")

        assert ping_parser.parser_name == parser_name
        assert stats.as_dict() == test_data.expected
        assert stats.icmp_replies == test_data.replies

    def test_empty(self, ping_parser):
        ping_parser.parse(
            dedent(
                """\
            PING google.com (216.58.196.238) 56(84) bytes of data.

            --- google.com ping statistics ---
            60 packets transmitted, 60 received, 0% packet loss, time 59153ms
            rtt min/avg/max/mdev = 61.425/99.731/212.597/27.566 ms
            """
            )
        )
        stats = ping_parser.parse("")

        assert stats.is_empty()

    @pytest.mark.parametrize(
        ["value"],
        [
            [
                PingResult(
                    stdout="",
                    stderr="ping: unknown: Temporary failure in name resolution\n",
                    returncode=2,
                )
            ]
        ],
    )
    def test_ping_failure(self, ping_parser, value):
        stats = ping_parser.parse(value)
        assert stats.is_empty()

    @pytest.mark.parametrize(
        ["value", "expected"],
        [[PING_FEDORA_EMPTY_BODY, ParseError], [PING_WINDOWS_INVALID, ParseError]],
    )
    def test_exception(self, ping_parser, value, expected):
        with pytest.raises(expected):
            ping_parser.parse(value)

class Test_PingParsing_as_tuple:
    def test_normal(self, ping_parser):
        stats = ping_parser.parse(DEBIAN_SUCCESS_0.value)
        result = stats.as_tuple()

        assert result.destination == "google.com"
        assert result.packet_transmit == 60
        assert result.packet_receive == 60
        assert result.packet_loss_count == 0
        assert result.packet_loss_rate == 0.0
        assert result.packet_duplicate_rate == 0
        assert result.packet_duplicate_count == 0
        assert result.rtt_min == 61.425
        assert result.rtt_avg == 99.731
        assert result.rtt_max == 212.597
        assert result.rtt_mdev == 27.566
        assert stats.icmp_replies == []


import inspect
import os
import warnings

import cv2
import numpy

from .logging import ddebug, debug, warn
from .types import Region
from .utils import to_unicode

class Frame(numpy.ndarray):
    """A frame of video.

    A ``Frame`` is what you get from `stbt.get_frame` and `stbt.frames`. It is
    a subclass of `numpy.ndarray`, which is the type that OpenCV uses to
    represent images. Data is stored in 8-bit, 3 channel BGR format.

    In addition to the members inherited from `numpy.ndarray`, ``Frame``
    defines the following attributes:

    :ivar float time: The wall-clock time when this video-frame was captured,
        as number of seconds since the unix epoch (1970-01-01T00:00:00Z). This
        is the same format used by the Python standard library function
        `time.time`.
    """
    def __new__(cls, array, dtype=None, order=None, time=None, _draw_sink=None):
        obj = numpy.asarray(array, dtype=dtype, order=order).view(cls)
        i = isinstance(array, Frame)
        if time is None and i:
            obj.time = array.time
        else:
            obj.time = time
        obj._draw_sink = _draw_sink or (i and array._draw_sink) or None
        return obj

    def __array_finalize__(self, obj):
        if obj is None:
            return
        self.time = getattr(obj, 'time', None)  # pylint: disable=attribute-defined-outside-init
        self._draw_sink = getattr(obj, '_draw_sink', None)  # pylint: disable=attribute-defined-outside-init

    def __repr__(self):
        if len(self.shape) == 3:
            dimensions = "%dx%dx%d" % (
                self.shape[1], self.shape[0], self.shape[2])  # pylint:disable=unsubscriptable-object

        elif len(self.shape) == 2:
            dimensions = "%dx%d" % (self.shape[1], self.shape[0])  # pylint:disable=unsubscriptable-object
        else:
            return super(Frame, self).__repr__()
        return "<Frame(time=%s, dimensions=%s)>" % (
            "None" if self.time is None else "%.3f" % self.time,
            dimensions)

    def __str__(self):
        return repr(self)

    @property
    def width(self):
        return self.shape[1]  # pylint:disable=unsubscriptable-object

    @property
    def height(self):
        return self.shape[0]  # pylint:disable=unsubscriptable-object

class Image(numpy.ndarray):
    """An image, possibly loaded from disk.

    This is a subclass of `numpy.ndarray`, which is the type that OpenCV uses
    to represent images.

    In addition to the members inherited from `numpy.ndarray`, ``Image``
    defines the following attributes:

    :vartype filename: str or None
    :ivar filename: The filename that was given to `stbt.load_image`.

    :vartype absolute_filename: str or None
    :ivar absolute_filename: The absolute path resolved by `stbt.load_image`.

    :vartype relative_filename: str or None
    :ivar relative_filename: The path resolved by `stbt.load_image`, relative
        to the root of the test-pack git repo.

    Added in v32.
    """
    def __new__(cls, array, dtype=None, order=None,
                filename=None, absolute_filename=None):

        obj = numpy.asarray(array, dtype=dtype, order=order).view(cls)
        i = isinstance(array, Image)
        obj.filename = filename or (i and array.filename) or None
        obj.absolute_filename = (absolute_filename or
                                 (i and array.absolute_filename) or
                                 None)
        obj.relative_filename = None

        if obj.absolute_filename is not None:
            import stbt_core
            root = getattr(stbt_core, "TEST_PACK_ROOT", None)
            if root is not None:
                obj.relative_filename = os.path.relpath(obj.absolute_filename,
                                                        root)
            else:
                obj.relative_filename = obj.absolute_filename

        return obj

    def __array_finalize__(self, obj):
        if obj is None:
            return
        self.filename = getattr(obj, "filename", None)
        self.relative_filename = getattr(obj, "relative_filename", None)
        self.absolute_filename = getattr(obj, "absolute_filename", None)

    def __repr__(self):
        if len(self.shape) == 3:
            dimensions = "%dx%dx%d" % (
                self.shape[1], self.shape[0], self.shape[2])  # pylint:disable=unsubscriptable-object
        elif len(self.shape) == 2:
            dimensions = "%dx%d" % (self.shape[1], self.shape[0])  # pylint:disable=unsubscriptable-object
        else:
            return super(Image, self).__repr__()
        if (self.relative_filename is None or
                self.relative_filename.startswith('../')):
            filename = self.absolute_filename
        else:
            filename = self.relative_filename
        return "<Image(filename=%r, dimensions=%s)>" % (filename, dimensions)

    def __str__(self):
        return repr(self)

    @property
    def width(self):
        return self.shape[1]  # pylint:disable=unsubscriptable-object

    @property
    def height(self):
        return self.shape[0]  # pylint:disable=unsubscriptable-object

def _frame_repr(frame):
    if frame is None:
        return "None"
    if isinstance(frame, (Image, Frame)):
        return repr(frame)
    if len(frame.shape) == 3:
        return "<%dx%dx%d>" % (frame.shape[1], frame.shape[0], frame.shape[2])
    else:
        return "<%dx%d>" % (frame.shape[1], frame.shape[0])

def crop(frame, region):
    """Returns an image containing the specified region of ``frame``.

    :type frame: `stbt.Frame` or `numpy.ndarray`
    :param frame: An image in OpenCV format (for example as returned by
      `frames`, `get_frame` and `load_image`, or the ``frame`` parameter of
      `MatchResult`).

    :type Region region: The region to crop.

    :returns: An OpenCV image (`numpy.ndarray`) containing the specified region
      of the source frame. This is a view onto the original data, so if you
      want to modify the cropped image call its ``copy()`` method first.
    """
    r = _validate_region(frame, region)
    return frame[r.y:r.bottom, r.x:r.right]

def _validate_region(frame, region):
    if region is None:
        raise TypeError(
            "'region=None' means an empty region. To analyse the entire "
            "frame use 'region=Region.ALL' (which is the default)")
    f = _image_region(frame)
    r = Region.intersect(f, region)
    if r is None:
        raise ValueError("%r doesn't overlap with the frame dimensions %ix%i"
                         % (region, frame.shape[1], frame.shape[0]))
    return r

def _image_region(image):
    s = image.shape
    return Region(0, 0, s[1], s[0])

def load_image(filename, flags=None, color_channels=None):
    """Find & read an image from disk.

    If given a relative filename, this will search in the directory of the
    Python file that called ``load_image``, then in the directory of that
    file's caller, etc. This allows you to use ``load_image`` in a helper
    function, and then call that helper function from a different Python file
    passing in a filename relative to the caller.

    Finally this will search in the current working directory. This allows
    loading an image that you had previously saved to disk during the same
    test run.

    This is the same lookup algorithm used by `stbt.match` and similar
    functions.

    :param str filename: A relative or absolute filename.

    :param flags: Flags to pass to :ocv:pyfunc:`cv2.imread`. Deprecated; use
      ``color_channels`` instead.

    :param Tuple[int] color_channels: Tuple of acceptable numbers of color
      channels for the output image: 1 for grayscale, 3 for color, and 4 for
      color with an alpha (transparency) channel. For example,
      ``color_channels=(3, 4)`` will accept color images with or without an
      alpha channel. Defaults to ``(3, 4)``.

      If the image doesn't match the specified ``color_channels`` it will be
      converted to the specified format.

    :rtype: stbt.Image
    :returns: An image in OpenCV format — that is, a `numpy.ndarray` of 8-bit
        values. With the default ``color_channels`` parameter this will be 3
        channels BGR, or 4 channels BGRA if the file has transparent pixels.
    :raises: `IOError` if the specified path doesn't exist or isn't a valid
        image file.

    * Changed in v30: Include alpha (transparency) channel if the file has
      transparent pixels.
    * Changed in v32: Return type is now `stbt.Image`, which is a
      `numpy.ndarray` sub-class with additional attributes ``filename``,
      ``relative_filename`` and ``absolute_filename``.
    * Changed in v32: Allows passing an image (`numpy.ndarray` or `stbt.Image`)
      instead of a string, in which case this function returns the given image.
    * Changed in v33: Added the ``color_channels`` parameter and deprecated
      ``flags``. The image will always be converted to the format specified by
      ``color_channels`` (previously it was only converted to the format
      specified by ``flags`` if it was given as a filename, not as a
      `stbt.Image` or numpy array).
    """
    if flags is not None:
        # Backwards compatibility
        if color_channels is not None:
            raise Exception(
                "flags cannot be specified at the same time as color_channels")
        if flags == cv2.IMREAD_GRAYSCALE:
            color_channels = (1,)
        elif flags == cv2.IMREAD_COLOR:
            color_channels = (3,)
        elif flags == cv2.IMREAD_UNCHANGED:
            color_channels = (1, 3, 4)
        else:
            raise Exception("Unsupported imread flags %s" % flags)
        warnings.warn(
            "load_image: flags=%s argument is deprecated. Use "
            "color_channels=%r instead" % (flags, color_channels),
            DeprecationWarning)

    if color_channels is None:
        color_channels = (3, 4)
    elif isinstance(color_channels, int):
        color_channels = (color_channels,)

    obj = filename
    if isinstance(obj, Image):
        img = obj
        filename = obj.filename
        absolute_filename = obj.absolute_filename
    elif isinstance(obj, numpy.ndarray):
        img = obj  # obj.filename etc. will be None
        filename = None
        absolute_filename = None
    else:
        filename = to_unicode(filename)
        absolute_filename = find_user_file(filename)
        if not absolute_filename:
            raise IOError(to_unicode("No such file: %s" % filename))
        absolute_filename = to_unicode(absolute_filename)
        if color_channels == (3,):
            flags = cv2.IMREAD_COLOR
        elif color_channels == (1,):
            flags = cv2.IMREAD_GRAYSCALE
        else:
            flags = cv2.IMREAD_UNCHANGED
        img = cv2.imread(to_unicode(absolute_filename), flags)
        if img is None:
            raise IOError(to_unicode("Failed to load image: %s" %
                                     absolute_filename))

    if len(img.shape) not in [2, 3]:
        raise ValueError(
            "Invalid shape for image: %r. Shape must have 2 or 3 elements" %
            (img.shape,))

    if img.dtype == numpy.uint16:
        warn("Image %s has 16 bits per channel. Converting to 8 bits."
             % filename)
        img = cv2.convertScaleAbs(img, alpha=1.0 / 256)
    elif img.dtype != numpy.uint8:
        raise ValueError("Image %s must be 8-bits per channel (got %s)"
                         % (filename, img.dtype))

    if len(img.shape) == 2:
        img = img.reshape(img.shape + (1,))
    c = img.shape[2]

    if c == 1:
        if 1 in color_channels:
            pass
        elif 3 in color_channels:
            img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
        elif 4 in color_channels:
            img = numpy.concatenate(
                (cv2.cvtColor(img, cv2.COLOR_GRAY2BGR),
                 numpy.full(img.shape[:2] + (1,), 255, dtype=img.dtype)),
                axis=2)
        else:
            raise ValueError(
                "Can only convert 1 channel image to 1, 3 or 4 channels")
    elif c == 3:
        if 3 in color_channels:
            pass
        elif 4 in color_channels:
            img = numpy.concatenate(
                (img, numpy.full(img.shape[:2] + (1,), 255, dtype=img.dtype)),
                axis=2)
        elif 1 in color_channels:
            img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            img = img.reshape(img.shape + (1,))
        else:
            raise ValueError(
                "Can only convert 3 channel image to 1, 3 or 4 channels")
    elif c == 4:
        if 4 in color_channels:
            # Remove alpha channel if it's 100% opaque
            if 3 in color_channels and numpy.all(img[:, :, 3] == 255):
                img = img[:, :, :3]
        elif 3 in color_channels:
            img = img[:, :, :3]
        elif 1 in color_channels:
            img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            img = img.reshape(img.shape + (1,))
        else:
            raise ValueError(
                "Can only convert 3 channel image to 1, 3 or 4 channels")
    else:
        raise ValueError(
            "load_image can only handle images with 1, 3 or 4 color channels. "
            "%s has %i channels" % (filename, c))

    assert img.shape[2] in color_channels
    if not isinstance(img, Image):
        img = Image(img, filename=filename, absolute_filename=absolute_filename)

    return img

def save_frame(image, filename):
    """Saves an OpenCV image to the specified file.

    Takes an image obtained from `get_frame` or from the `screenshot`
    property of `MatchTimeout` or `MotionTimeout`.
    """
    cv2.imwrite(filename, image)

"""
    Find the smallest region that contains all the non-zero pixels in an image.

    >>> pixel_bounding_box(numpy.array([[0]], dtype=numpy.uint8))
    >>> pixel_bounding_box(numpy.array([[1]], dtype=numpy.uint8))
    Region(x=0, y=0, right=1, bottom=1)
    >>> a = numpy.array([
    ...     [0, 0, 0, 0],
    ...     [0, 1, 1, 1],
    ...     [0, 1, 1, 1],
    ...     [0, 0, 0, 0],
    ... ], dtype=numpy.uint8)
    >>> pixel_bounding_box(a)
    Region(x=1, y=1, right=4, bottom=3)
    >>> pixel_bounding_box(numpy.stack([
    ...     numpy.zeros((4, 4), dtype=numpy.uint8),
    ...     numpy.zeros((4, 4), dtype=numpy.uint8),
    ...     a],
    ...     axis=-1))
    Region(x=1, y=1, right=4, bottom=3)
    >>> pixel_bounding_box(numpy.array([
    ...     [0, 0, 0, 0, 0, 0],
    ...     [0, 0, 0, 1, 0, 0],
    ...     [0, 1, 0, 0, 0, 0],
    ...     [0, 0, 0, 0, 1, 0],
    ...     [0, 0, 0, 0, 0, 0],
    ...     [0, 0, 1, 0, 0, 0],
    ...     [0, 0, 0, 0, 0, 0]
    ... ], dtype=numpy.uint8))
    Region(x=1, y=1, right=5, bottom=6)
    """
    if len(img.shape) == 2:
        pass
    elif len(img.shape) == 3 and img.shape[2] == 3:
        img = img.max(axis=2)
    else:
        raise ValueError("Single-channel or 3-channel (BGR) image required. "
                         "Provided image has shape %r" % (img.shape,))

    out = [None, None, None, None]

    for axis in (0, 1):
        flat = numpy.any(img, axis=axis)
        indices = numpy.where(flat)[0]
        if len(indices) == 0:
            return None
        out[axis] = int(indices[0])
        out[axis + 2] = int(indices[-1] + 1)

    return Region.from_extents(*out)

def find_user_file(filename):
    """Searches for the given filename and returns the full path.

    Searches in the directory of the script that called `load_image` (or
    `match`, etc), then in the directory of that script's caller, etc.
    Falls back to searching the current working directory.

    :returns: Absolute filename, or None if it can't find the file.
    """
    if os.path.isabs(filename) and os.path.isfile(filename):
        return filename

    # Start searching from the first parent stack-frame that is outside of
    # the _stbt installation directory (this file's directory). We can ignore
    # the first 2 stack-frames:
    # * stack()[2] is load_image's caller -- load_image can be called
    #   directly from the user script, or indirectly via stbt.match so we still
    #   need to check until we're outside of the _stbt directory.

    filename = to_unicode(filename)
    _stbt_dir = os.path.abspath(os.path.dirname(__file__))
    caller = inspect.currentframe()
    try:
        # Skip this frame and the parent:
        caller = caller.f_back
        caller = caller.f_back
        while caller:
            caller_dir = os.path.abspath(
                os.path.dirname(inspect.getframeinfo(caller).filename))
            if not caller_dir.startswith(_stbt_dir):
                caller_path = os.path.join(caller_dir, filename)
                if os.path.isfile(caller_path):
                    ddebug("Resolved relative path %r to %r" % (
                        filename, caller_path))
                    return caller_path
            caller = caller.f_back
    finally:
        # Avoid circular references between stack frame objects and themselves
        # for more deterministic GC.  See
        del caller

    # Fall back to image from cwd, to allow loading an image saved previously
    # during the same test-run.
    if os.path.isfile(filename):
        abspath = os.path.abspath(filename)
        ddebug("Resolved relative path %r to %r" % (filename, abspath))
        return abspath

    return None

def limit_time(frames, duration_secs):
    """
    Adapts a frame iterator such that it will return EOS after `duration_secs`
    worth of video has been read.
    """
    import time
    end_time = time.time() + duration_secs
    for frame in frames:
        if frame.time > end_time:
            debug("timed out: %.3f > %.3f" % (frame.time, end_time))
            break
        else:
            yield frame


obj = numpy.asarray(array, dtype=dtype, order=order).view(cls)
        i = isinstance(array, Frame)
        if time is None and i:
            obj.time = array.time
        else:
            obj.time = time
        obj._draw_sink = _draw_sink or (i and array._draw_sink) or None
        return obj

import inspect
import os
import warnings

import cv2
import numpy

from .logging import ddebug, debug, warn
from .types import Region
from .utils import to_unicode

class Frame(numpy.ndarray):
    """A frame of video.

    A ``Frame`` is what you get from `stbt.get_frame` and `stbt.frames`. It is
    a subclass of `numpy.ndarray`, which is the type that OpenCV uses to
    represent images. Data is stored in 8-bit, 3 channel BGR format.

    In addition to the members inherited from `numpy.ndarray`, ``Frame``
    defines the following attributes:

    :ivar float time: The wall-clock time when this video-frame was captured,
        as number of seconds since the unix epoch (1970-01-01T00:00:00Z). This
        is the same format used by the Python standard library function
        `time.time`.
    """
    def __new__(cls, array, dtype=None, order=None, time=None, _draw_sink=None):

    def __array_finalize__(self, obj):
        if obj is None:
            return
        self.time = getattr(obj, 'time', None)  # pylint: disable=attribute-defined-outside-init
        self._draw_sink = getattr(obj, '_draw_sink', None)  # pylint: disable=attribute-defined-outside-init

    def __repr__(self):
        if len(self.shape) == 3:
            dimensions = "%dx%dx%d" % (
                self.shape[1], self.shape[0], self.shape[2])  # pylint:disable=unsubscriptable-object

        elif len(self.shape) == 2:
            dimensions = "%dx%d" % (self.shape[1], self.shape[0])  # pylint:disable=unsubscriptable-object
        else:
            return super(Frame, self).__repr__()
        return "<Frame(time=%s, dimensions=%s)>" % (
            "None" if self.time is None else "%.3f" % self.time,
            dimensions)

    def __str__(self):
        return repr(self)

    @property
    def width(self):
        return self.shape[1]  # pylint:disable=unsubscriptable-object

    @property
    def height(self):
        return self.shape[0]  # pylint:disable=unsubscriptable-object

class Image(numpy.ndarray):
    """An image, possibly loaded from disk.

    This is a subclass of `numpy.ndarray`, which is the type that OpenCV uses
    to represent images.

    In addition to the members inherited from `numpy.ndarray`, ``Image``
    defines the following attributes:

    :vartype filename: str or None
    :ivar filename: The filename that was given to `stbt.load_image`.

    :vartype absolute_filename: str or None
    :ivar absolute_filename: The absolute path resolved by `stbt.load_image`.

    :vartype relative_filename: str or None
    :ivar relative_filename: The path resolved by `stbt.load_image`, relative
        to the root of the test-pack git repo.

    Added in v32.
    """
    def __new__(cls, array, dtype=None, order=None,
                filename=None, absolute_filename=None):

        obj = numpy.asarray(array, dtype=dtype, order=order).view(cls)
        i = isinstance(array, Image)
        obj.filename = filename or (i and array.filename) or None
        obj.absolute_filename = (absolute_filename or
                                 (i and array.absolute_filename) or
                                 None)
        obj.relative_filename = None

        if obj.absolute_filename is not None:
            import stbt_core
            root = getattr(stbt_core, "TEST_PACK_ROOT", None)
            if root is not None:
                obj.relative_filename = os.path.relpath(obj.absolute_filename,
                                                        root)
            else:
                obj.relative_filename = obj.absolute_filename

        return obj

    def __array_finalize__(self, obj):
        if obj is None:
            return
        self.filename = getattr(obj, "filename", None)
        self.relative_filename = getattr(obj, "relative_filename", None)
        self.absolute_filename = getattr(obj, "absolute_filename", None)

    def __repr__(self):
        if len(self.shape) == 3:
            dimensions = "%dx%dx%d" % (
                self.shape[1], self.shape[0], self.shape[2])  # pylint:disable=unsubscriptable-object
        elif len(self.shape) == 2:
            dimensions = "%dx%d" % (self.shape[1], self.shape[0])  # pylint:disable=unsubscriptable-object
        else:
            return super(Image, self).__repr__()
        if (self.relative_filename is None or
                self.relative_filename.startswith('../')):
            filename = self.absolute_filename
        else:
            filename = self.relative_filename
        return "<Image(filename=%r, dimensions=%s)>" % (filename, dimensions)

    def __str__(self):
        return repr(self)

    @property
    def width(self):
        return self.shape[1]  # pylint:disable=unsubscriptable-object

    @property
    def height(self):
        return self.shape[0]  # pylint:disable=unsubscriptable-object

def _frame_repr(frame):
    if frame is None:
        return "None"
    if isinstance(frame, (Image, Frame)):
        return repr(frame)
    if len(frame.shape) == 3:
        return "<%dx%dx%d>" % (frame.shape[1], frame.shape[0], frame.shape[2])
    else:
        return "<%dx%d>" % (frame.shape[1], frame.shape[0])

def crop(frame, region):
    """Returns an image containing the specified region of ``frame``.

    :type frame: `stbt.Frame` or `numpy.ndarray`
    :param frame: An image in OpenCV format (for example as returned by
      `frames`, `get_frame` and `load_image`, or the ``frame`` parameter of
      `MatchResult`).

    :type Region region: The region to crop.

    :returns: An OpenCV image (`numpy.ndarray`) containing the specified region
      of the source frame. This is a view onto the original data, so if you
      want to modify the cropped image call its ``copy()`` method first.
    """
    r = _validate_region(frame, region)
    return frame[r.y:r.bottom, r.x:r.right]

def _validate_region(frame, region):
    if region is None:
        raise TypeError(
            "'region=None' means an empty region. To analyse the entire "
            "frame use 'region=Region.ALL' (which is the default)")
    f = _image_region(frame)
    r = Region.intersect(f, region)
    if r is None:
        raise ValueError("%r doesn't overlap with the frame dimensions %ix%i"
                         % (region, frame.shape[1], frame.shape[0]))
    return r

def _image_region(image):
    s = image.shape
    return Region(0, 0, s[1], s[0])

def load_image(filename, flags=None, color_channels=None):
    """Find & read an image from disk.

    If given a relative filename, this will search in the directory of the
    Python file that called ``load_image``, then in the directory of that
    file's caller, etc. This allows you to use ``load_image`` in a helper
    function, and then call that helper function from a different Python file
    passing in a filename relative to the caller.

    Finally this will search in the current working directory. This allows
    loading an image that you had previously saved to disk during the same
    test run.

    This is the same lookup algorithm used by `stbt.match` and similar
    functions.

    :param str filename: A relative or absolute filename.

    :param flags: Flags to pass to :ocv:pyfunc:`cv2.imread`. Deprecated; use
      ``color_channels`` instead.

    :param Tuple[int] color_channels: Tuple of acceptable numbers of color
      channels for the output image: 1 for grayscale, 3 for color, and 4 for
      color with an alpha (transparency) channel. For example,
      ``color_channels=(3, 4)`` will accept color images with or without an
      alpha channel. Defaults to ``(3, 4)``.

      If the image doesn't match the specified ``color_channels`` it will be
      converted to the specified format.

    :rtype: stbt.Image
    :returns: An image in OpenCV format — that is, a `numpy.ndarray` of 8-bit
        values. With the default ``color_channels`` parameter this will be 3
        channels BGR, or 4 channels BGRA if the file has transparent pixels.
    :raises: `IOError` if the specified path doesn't exist or isn't a valid
        image file.

    * Changed in v30: Include alpha (transparency) channel if the file has
      transparent pixels.
    * Changed in v32: Return type is now `stbt.Image`, which is a
      `numpy.ndarray` sub-class with additional attributes ``filename``,
      ``relative_filename`` and ``absolute_filename``.
    * Changed in v32: Allows passing an image (`numpy.ndarray` or `stbt.Image`)
      instead of a string, in which case this function returns the given image.
    * Changed in v33: Added the ``color_channels`` parameter and deprecated
      ``flags``. The image will always be converted to the format specified by
      ``color_channels`` (previously it was only converted to the format
      specified by ``flags`` if it was given as a filename, not as a
      `stbt.Image` or numpy array).
    """
    if flags is not None:
        # Backwards compatibility
        if color_channels is not None:
            raise Exception(
                "flags cannot be specified at the same time as color_channels")
        if flags == cv2.IMREAD_GRAYSCALE:
            color_channels = (1,)
        elif flags == cv2.IMREAD_COLOR:
            color_channels = (3,)
        elif flags == cv2.IMREAD_UNCHANGED:
            color_channels = (1, 3, 4)
        else:
            raise Exception("Unsupported imread flags %s" % flags)
        warnings.warn(
            "load_image: flags=%s argument is deprecated. Use "
            "color_channels=%r instead" % (flags, color_channels),
            DeprecationWarning)

    if color_channels is None:
        color_channels = (3, 4)
    elif isinstance(color_channels, int):
        color_channels = (color_channels,)

    obj = filename
    if isinstance(obj, Image):
        img = obj
        filename = obj.filename
        absolute_filename = obj.absolute_filename
    elif isinstance(obj, numpy.ndarray):
        img = obj  # obj.filename etc. will be None
        filename = None
        absolute_filename = None
    else:
        filename = to_unicode(filename)
        absolute_filename = find_user_file(filename)
        if not absolute_filename:
            raise IOError(to_unicode("No such file: %s" % filename))
        absolute_filename = to_unicode(absolute_filename)
        if color_channels == (3,):
            flags = cv2.IMREAD_COLOR
        elif color_channels == (1,):
            flags = cv2.IMREAD_GRAYSCALE
        else:
            flags = cv2.IMREAD_UNCHANGED
        img = cv2.imread(to_unicode(absolute_filename), flags)
        if img is None:
            raise IOError(to_unicode("Failed to load image: %s" %
                                     absolute_filename))

    if len(img.shape) not in [2, 3]:
        raise ValueError(
            "Invalid shape for image: %r. Shape must have 2 or 3 elements" %
            (img.shape,))

    if img.dtype == numpy.uint16:
        warn("Image %s has 16 bits per channel. Converting to 8 bits."
             % filename)
        img = cv2.convertScaleAbs(img, alpha=1.0 / 256)
    elif img.dtype != numpy.uint8:
        raise ValueError("Image %s must be 8-bits per channel (got %s)"
                         % (filename, img.dtype))

    if len(img.shape) == 2:
        img = img.reshape(img.shape + (1,))
    c = img.shape[2]

    if c == 1:
        if 1 in color_channels:
            pass
        elif 3 in color_channels:
            img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
        elif 4 in color_channels:
            img = numpy.concatenate(
                (cv2.cvtColor(img, cv2.COLOR_GRAY2BGR),
                 numpy.full(img.shape[:2] + (1,), 255, dtype=img.dtype)),
                axis=2)
        else:
            raise ValueError(
                "Can only convert 1 channel image to 1, 3 or 4 channels")
    elif c == 3:
        if 3 in color_channels:
            pass
        elif 4 in color_channels:
            img = numpy.concatenate(
                (img, numpy.full(img.shape[:2] + (1,), 255, dtype=img.dtype)),
                axis=2)
        elif 1 in color_channels:
            img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            img = img.reshape(img.shape + (1,))
        else:
            raise ValueError(
                "Can only convert 3 channel image to 1, 3 or 4 channels")
    elif c == 4:
        if 4 in color_channels:
            # Remove alpha channel if it's 100% opaque
            if 3 in color_channels and numpy.all(img[:, :, 3] == 255):
                img = img[:, :, :3]
        elif 3 in color_channels:
            img = img[:, :, :3]
        elif 1 in color_channels:
            img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            img = img.reshape(img.shape + (1,))
        else:
            raise ValueError(
                "Can only convert 3 channel image to 1, 3 or 4 channels")
    else:
        raise ValueError(
            "load_image can only handle images with 1, 3 or 4 color channels. "
            "%s has %i channels" % (filename, c))

    assert img.shape[2] in color_channels
    if not isinstance(img, Image):
        img = Image(img, filename=filename, absolute_filename=absolute_filename)

    return img

def save_frame(image, filename):
    """Saves an OpenCV image to the specified file.

    Takes an image obtained from `get_frame` or from the `screenshot`
    property of `MatchTimeout` or `MotionTimeout`.
    """
    cv2.imwrite(filename, image)

def pixel_bounding_box(img):
    """
    Find the smallest region that contains all the non-zero pixels in an image.

    >>> pixel_bounding_box(numpy.array([[0]], dtype=numpy.uint8))
    >>> pixel_bounding_box(numpy.array([[1]], dtype=numpy.uint8))
    Region(x=0, y=0, right=1, bottom=1)
    >>> a = numpy.array([
    ...     [0, 0, 0, 0],
    ...     [0, 1, 1, 1],
    ...     [0, 1, 1, 1],
    ...     [0, 0, 0, 0],
    ... ], dtype=numpy.uint8)
    >>> pixel_bounding_box(a)
    Region(x=1, y=1, right=4, bottom=3)
    >>> pixel_bounding_box(numpy.stack([
    ...     numpy.zeros((4, 4), dtype=numpy.uint8),
    ...     numpy.zeros((4, 4), dtype=numpy.uint8),
    ...     a],
    ...     axis=-1))
    Region(x=1, y=1, right=4, bottom=3)
    >>> pixel_bounding_box(numpy.array([
    ...     [0, 0, 0, 0, 0, 0],
    ...     [0, 0, 0, 1, 0, 0],
    ...     [0, 1, 0, 0, 0, 0],
    ...     [0, 0, 0, 0, 1, 0],
    ...     [0, 0, 0, 0, 0, 0],
    ...     [0, 0, 1, 0, 0, 0],
    ...     [0, 0, 0, 0, 0, 0]
    ... ], dtype=numpy.uint8))
    Region(x=1, y=1, right=5, bottom=6)
    """
    if len(img.shape) == 2:
        pass
    elif len(img.shape) == 3 and img.shape[2] == 3:
        img = img.max(axis=2)
    else:
        raise ValueError("Single-channel or 3-channel (BGR) image required. "
                         "Provided image has shape %r" % (img.shape,))

    out = [None, None, None, None]

    for axis in (0, 1):
        flat = numpy.any(img, axis=axis)
        indices = numpy.where(flat)[0]
        if len(indices) == 0:
            return None
        out[axis] = int(indices[0])
        out[axis + 2] = int(indices[-1] + 1)

    return Region.from_extents(*out)

def find_user_file(filename):
    """Searches for the given filename and returns the full path.

    Searches in the directory of the script that called `load_image` (or
    `match`, etc), then in the directory of that script's caller, etc.
    Falls back to searching the current working directory.

    :returns: Absolute filename, or None if it can't find the file.
    """
    if os.path.isabs(filename) and os.path.isfile(filename):
        return filename

    # Start searching from the first parent stack-frame that is outside of
    # the _stbt installation directory (this file's directory). We can ignore
    # the first 2 stack-frames:
    # * stack()[2] is load_image's caller -- load_image can be called
    #   directly from the user script, or indirectly via stbt.match so we still
    #   need to check until we're outside of the _stbt directory.

    filename = to_unicode(filename)
    _stbt_dir = os.path.abspath(os.path.dirname(__file__))
    caller = inspect.currentframe()
    try:
        # Skip this frame and the parent:
        caller = caller.f_back
        caller = caller.f_back
        while caller:
            caller_dir = os.path.abspath(
                os.path.dirname(inspect.getframeinfo(caller).filename))
            if not caller_dir.startswith(_stbt_dir):
                caller_path = os.path.join(caller_dir, filename)
                if os.path.isfile(caller_path):
                    ddebug("Resolved relative path %r to %r" % (
                        filename, caller_path))
                    return caller_path
            caller = caller.f_back
    finally:
        # Avoid circular references between stack frame objects and themselves
        # for more deterministic GC.  See
        del caller

    # Fall back to image from cwd, to allow loading an image saved previously
    # during the same test-run.
    if os.path.isfile(filename):
        abspath = os.path.abspath(filename)
        ddebug("Resolved relative path %r to %r" % (filename, abspath))
        return abspath

    return None

def limit_time(frames, duration_secs):
    """
    Adapts a frame iterator such that it will return EOS after `duration_secs`
    worth of video has been read.
    """
    import time
    end_time = time.time() + duration_secs
    for frame in frames:
        if frame.time > end_time:
            debug("timed out: %.3f > %.3f" % (frame.time, end_time))
            break
        else:
            yield frame