# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from datetime import tzinfo
from inspect import cleandoc

# Disable logging for a cleaner testing output
import logging
import os
import re
import sys
import time
from unittest import mock
from urllib.parse import unquote

from apprise import NotificationManager, utils
from apprise.tag import AppriseTag

logging.disable(logging.CRITICAL)

# Ensure we don't create .pyc files for these tests
sys.dont_write_bytecode = True

# Grant access to our Notification Manager Singleton
N_MGR = NotificationManager()


def test_parse_qsd():
    "utils: parse_qsd() testing"

    result = utils.parse.parse_qsd("a=1&b=&c&d=abcd")
    assert isinstance(result, dict)
    assert len(result) == 4
    assert "qsd" in result
    assert "qsd+" in result
    assert "qsd-" in result
    assert "qsd:" in result

    assert len(result["qsd"]) == 4
    assert "a" in result["qsd"]
    assert "b" in result["qsd"]
    assert "c" in result["qsd"]
    assert "d" in result["qsd"]

    assert len(result["qsd-"]) == 0
    assert len(result["qsd+"]) == 0
    assert len(result["qsd:"]) == 0


def test_parse_url_general():
    "utils: parse_url() testing"

    result = utils.parse.parse_url("http://hostname")
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # GitHub Ticket 1234 - Unparseable Hostname
    result = utils.parse.parse_url("http://5t4m59hl-34343.euw.devtunnels.ms")
    assert result["schema"] == "http"
    assert result["host"] == "5t4m59hl-34343.euw.devtunnels.ms"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://5t4m59hl-34343.euw.devtunnels.ms"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url("http://hostname/")
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["query"] is None
    assert result["url"] == "http://hostname/"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # colon after hostname without port number is no good
    assert utils.parse.parse_url("http://hostname:") is None

    # An invalid port
    result = utils.parse.parse_url(
        "http://hostname:invalid", verify_host=False, strict_port=True
    )
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == "invalid"
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:invalid"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # However if we don't verify the host, it is okay
    result = utils.parse.parse_url("http://hostname:", verify_host=False)
    assert result["schema"] == "http"
    assert result["host"] == "hostname:"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # A port of Zero is not valid with strict port checking
    assert utils.parse.parse_url("http://hostname:0", strict_port=True) is None

    # Without strict port checking however, it is okay
    result = utils.parse.parse_url("http://hostname:0", strict_port=False)
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == 0
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:0"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # A negative port is not valid
    assert (
        utils.parse.parse_url("http://hostname:-92", strict_port=True) is None
    )
    result = utils.parse.parse_url(
        "http://hostname:-92", verify_host=False, strict_port=True
    )
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == -92
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:-92"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # A port that is too large is not valid
    assert (
        utils.parse.parse_url("http://hostname:65536", strict_port=True)
        is None
    )

    # This is an accetable port (the maximum)
    result = utils.parse.parse_url("http://hostname:65535", strict_port=True)
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == 65535
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:65535"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # This is an accetable port (the maximum)
    result = utils.parse.parse_url("http://hostname:1", strict_port=True)
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == 1
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:1"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # A port that was identfied as a string is invalid
    assert (
        utils.parse.parse_url("http://hostname:invalid", strict_port=True)
        is None
    )
    result = utils.parse.parse_url(
        "http://hostname:invalid", verify_host=False, strict_port=True
    )
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == "invalid"
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:invalid"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url(
        "http://hostname:invalid", verify_host=False, strict_port=False
    )
    assert result["schema"] == "http"
    assert result["host"] == "hostname:invalid"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:invalid"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url(
        "http://hostname:invalid?key=value&-minuskey=mvalue",
        verify_host=False,
        strict_port=False,
    )
    assert result["schema"] == "http"
    assert result["host"] == "hostname:invalid"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:invalid"
    assert unquote(result["qsd"]["-minuskey"]) == "mvalue"
    assert unquote(result["qsd"]["key"]) == "value"
    assert unquote(result["qsd-"]["minuskey"]) == "mvalue"
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # Handling of floats
    assert (
        utils.parse.parse_url("http://hostname:4.2", strict_port=True) is None
    )
    result = utils.parse.parse_url(
        "http://hostname:4.2", verify_host=False, strict_port=True
    )
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == "4.2"
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:4.2"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # A Port of zero is not acceptable for a regular hostname
    assert utils.parse.parse_url("http://hostname:0", strict_port=True) is None

    # No host verification (zero is an acceptable port when this is the case
    result = utils.parse.parse_url("http://hostname:0", verify_host=False)
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == 0
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:0"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url(
        "http://[2001:db8:002a:3256:adfe:05c0:0003:0006]:8080",
        verify_host=False,
    )
    assert result["schema"] == "http"
    assert result["host"] == "[2001:db8:002a:3256:adfe:05c0:0003:0006]"
    assert result["port"] == 8080
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert (
        result["url"] == "http://[2001:db8:002a:3256:adfe:05c0:0003:0006]:8080"
    )
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url(
        "http://hostname:0", verify_host=False, strict_port=True
    )
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == 0
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://hostname:0"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url("http://hostname/?-KeY=Value")
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["query"] is None
    assert result["url"] == "http://hostname/"
    assert "-key" in result["qsd"]
    assert unquote(result["qsd"]["-key"]) == "Value"
    assert "KeY" in result["qsd-"]
    assert unquote(result["qsd-"]["KeY"]) == "Value"
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url("http://hostname/?+KeY=Value")
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["query"] is None
    assert result["url"] == "http://hostname/"
    assert "+key" in result["qsd"]
    assert "KeY" in result["qsd+"]
    assert result["qsd+"]["KeY"] == "Value"
    assert result["qsd-"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url("http://hostname/?:kEy=vALUE")
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["query"] is None
    assert result["url"] == "http://hostname/"
    assert ":key" in result["qsd"]
    assert "kEy" in result["qsd:"]
    assert result["qsd:"]["kEy"] == "vALUE"
    assert result["qsd+"] == {}
    assert result["qsd-"] == {}

    result = utils.parse.parse_url(
        "http://hostname/?+KeY=ValueA&-kEy=ValueB&KEY=Value%20+C&:colon=y"
    )
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["query"] is None
    assert result["url"] == "http://hostname/"
    assert "+key" in result["qsd"]
    assert "-key" in result["qsd"]
    assert ":colon" in result["qsd"]
    assert result["qsd:"]["colon"] == "y"
    assert "key" in result["qsd"]
    assert "KeY" in result["qsd+"]
    assert result["qsd+"]["KeY"] == "ValueA"
    assert "kEy" in result["qsd-"]
    assert result["qsd-"]["kEy"] == "ValueB"
    assert result["qsd"]["key"] == "Value +C"
    assert result["qsd"]["+key"] == result["qsd+"]["KeY"]
    assert result["qsd"]["-key"] == result["qsd-"]["kEy"]

    result = utils.parse.parse_url(
        "http://hostname/?+KeY=ValueA&-kEy=ValueB&KEY=Value%20+C&:colon=y",
        plus_to_space=True,
    )
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["query"] is None
    assert result["url"] == "http://hostname/"
    assert "+key" in result["qsd"]
    assert "-key" in result["qsd"]
    assert ":colon" in result["qsd"]
    assert result["qsd:"]["colon"] == "y"
    assert "key" in result["qsd"]
    assert "KeY" in result["qsd+"]
    assert result["qsd+"]["KeY"] == "ValueA"
    assert "kEy" in result["qsd-"]
    assert result["qsd-"]["kEy"] == "ValueB"
    assert result["qsd"]["key"] == "Value  C"
    assert result["qsd"]["+key"] == result["qsd+"]["KeY"]
    assert result["qsd"]["-key"] == result["qsd-"]["kEy"]

    result = utils.parse.parse_url("http://hostname////")
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["query"] is None
    assert result["url"] == "http://hostname/"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url("http://hostname:40////")
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == 40
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["query"] is None
    assert result["url"] == "http://hostname:40/"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url("HTTP://HoStNaMe:40/test.php")
    assert result["schema"] == "http"
    assert result["host"] == "HoStNaMe"
    assert result["port"] == 40
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/test.php"
    assert result["path"] == "/"
    assert result["query"] == "test.php"
    assert result["url"] == "http://HoStNaMe:40/test.php"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url("HTTPS://user@hostname/test.py")
    assert result["schema"] == "https"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] == "user"
    assert result["password"] is None
    assert result["fullpath"] == "/test.py"
    assert result["path"] == "/"
    assert result["query"] == "test.py"
    assert result["url"] == "https://user@hostname/test.py"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url("  HTTPS://///user@@@hostname///test.py  ")
    assert result["schema"] == "https"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] == "user"
    assert result["password"] is None
    assert result["fullpath"] == "/test.py"
    assert result["path"] == "/"
    assert result["query"] == "test.py"
    assert result["url"] == "https://user@hostname/test.py"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url(
        "HTTPS://user:password@otherHost/full///path/name/",
    )
    assert result["schema"] == "https"
    assert result["host"] == "otherHost"
    assert result["port"] is None
    assert result["user"] == "user"
    assert result["password"] == "password"
    assert result["fullpath"] == "/full/path/name/"
    assert result["path"] == "/full/path/name/"
    assert result["query"] is None
    assert result["url"] == "https://user:password@otherHost/full/path/name/"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url(
        "HTTPS://hostname/a/path/ending/with/slash/?key=value",
    )
    assert result["schema"] == "https"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/a/path/ending/with/slash/"
    assert result["path"] == "/a/path/ending/with/slash/"
    assert result["query"] is None
    assert result["url"] == "https://hostname/a/path/ending/with/slash/"
    assert result["qsd"] == {"key": "value"}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # Handle garbage
    assert utils.parse.parse_url(None) is None

    result = utils.parse.parse_url(
        "mailto://user:password@otherHost/lead2gold@gmail.com"
        + "?from=test@test.com&name=Chris%20Caron&format=text"
    )
    assert result["schema"] == "mailto"
    assert result["host"] == "otherHost"
    assert result["port"] is None
    assert result["user"] == "user"
    assert result["password"] == "password"
    assert unquote(result["fullpath"]) == "/lead2gold@gmail.com"
    assert result["path"] == "/"
    assert unquote(result["query"]) == "lead2gold@gmail.com"
    assert (
        unquote(result["url"])
        == "mailto://user:password@otherHost/lead2gold@gmail.com"
    )
    assert len(result["qsd"]) == 3
    assert "name" in result["qsd"]
    assert unquote(result["qsd"]["name"]) == "Chris Caron"
    assert "from" in result["qsd"]
    assert unquote(result["qsd"]["from"]) == "test@test.com"
    assert "format" in result["qsd"]
    assert unquote(result["qsd"]["format"]) == "text"
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # Test Passwords with question marks ?; not supported
    result = utils.parse.parse_url("http://user:pass.with.?question@host")
    assert result is None

    # just hostnames
    result = utils.parse.parse_url("nuxref.com")
    assert result["schema"] == "http"
    assert result["host"] == "nuxref.com"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "http://nuxref.com"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # just host and path
    result = utils.parse.parse_url("invalid/host")
    assert result["schema"] == "http"
    assert result["host"] == "invalid"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/host"
    assert result["path"] == "/"
    assert result["query"] == "host"
    assert result["url"] == "http://invalid/host"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # just all out invalid
    assert utils.parse.parse_url("?") is None
    assert utils.parse.parse_url("/") is None

    # Test some illegal strings
    result = utils.parse.parse_url(object, verify_host=False)
    assert result is None
    result = utils.parse.parse_url(None, verify_host=False)
    assert result is None

    # Just a schema; invalid host
    result = utils.parse.parse_url("test://")
    assert result is None

    # Do it again without host validation
    result = utils.parse.parse_url("test://", verify_host=False)
    assert result["schema"] == "test"
    # It's worth noting that the hostname is an empty string and is NEVER set
    # to None if it wasn't specified.
    assert result["host"] == ""
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    assert result["url"] == "test://"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url("testhostname")
    assert result["schema"] == "http"
    assert result["host"] == "testhostname"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    # The default_schema kicks in here
    assert result["url"] == "http://testhostname"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url("example.com", default_schema="unknown")
    assert result["schema"] == "unknown"
    assert result["host"] == "example.com"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    # The default_schema kicks in here
    assert result["url"] == "unknown://example.com"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # An empty string without a hostame is still valid if verify_host is set
    result = utils.parse.parse_url("", verify_host=False)
    assert result["schema"] == "http"
    assert result["host"] == ""
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] is None
    assert result["path"] is None
    assert result["query"] is None
    # The default_schema kicks in here
    assert result["url"] == "http://"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # A messed up URL
    result = utils.parse.parse_url("test://:@/", verify_host=False)
    assert result["schema"] == "test"
    assert result["host"] == ""
    assert result["port"] is None
    assert result["user"] == ""
    assert result["password"] == ""
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["query"] is None
    assert result["url"] == "test://:@/"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    result = utils.parse.parse_url(
        "crazy://:@//_/@^&/jack.json", verify_host=False
    )
    assert result["schema"] == "crazy"
    assert result["host"] == ""
    assert result["port"] is None
    assert result["user"] == ""
    assert result["password"] == ""
    assert unquote(result["fullpath"]) == "/_/@^&/jack.json"
    assert unquote(result["path"]) == "/_/@^&/"
    assert result["query"] == "jack.json"
    assert unquote(result["url"]) == "crazy://:@/_/@^&/jack.json"
    assert result["qsd"] == {}
    assert result["qsd-"] == {}
    assert result["qsd+"] == {}
    assert result["qsd:"] == {}

    # Sanitizing
    result = utils.parse.parse_url(
        "hTTp://hostname/?+KeY=ValueA&-kEy=ValueB&KEY=Value%20+C&:cOlON=YeS",
        sanitize=False,
    )

    assert len(result["qsd-"]) == 1
    assert len(result["qsd+"]) == 1
    assert len(result["qsd"]) == 4
    assert len(result["qsd:"]) == 1

    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["query"] is None
    assert result["url"] == "http://hostname/"
    assert "+KeY" in result["qsd"]
    assert "-kEy" in result["qsd"]
    assert ":cOlON" in result["qsd"]
    assert result["qsd:"]["cOlON"] == "YeS"
    assert "key" not in result["qsd"]
    assert "KeY" in result["qsd+"]
    assert result["qsd+"]["KeY"] == "ValueA"
    assert "kEy" in result["qsd-"]
    assert result["qsd-"]["kEy"] == "ValueB"
    assert result["qsd"]["KEY"] == "Value +C"
    assert result["qsd"]["+KeY"] == result["qsd+"]["KeY"]
    assert result["qsd"]["-kEy"] == result["qsd-"]["kEy"]

    # Testing Defect 1264 - whitespaces in url
    result = utils.parse.parse_url(
        "posts://example.com/my endpoint?-token=ab cdefg"
    )

    assert len(result["qsd-"]) == 1
    assert len(result["qsd+"]) == 0
    assert len(result["qsd"]) == 1
    assert len(result["qsd:"]) == 0

    assert result["schema"] == "posts"
    assert result["host"] == "example.com"
    assert result["port"] is None
    assert result["user"] is None
    assert result["password"] is None
    assert result["fullpath"] == "/my%20endpoint"
    assert result["path"] == "/"
    assert result["query"] == "my%20endpoint"
    assert result["url"] == "posts://example.com/my%20endpoint"
    assert "-token" in result["qsd"]
    assert result["qsd-"]["token"] == "ab cdefg"

    # Testing Defect 1672 - reserved characters in URL paths
    url = "https://cdn.jsdelivr.net/gh/Cleanuparr/Cleanuparr@main/Logo/256.png"
    result = utils.parse.parse_url(url)
    assert result["fullpath"] == "/gh/Cleanuparr/Cleanuparr@main/Logo/256.png"
    assert result["url"] == url

    # RFC 3986 path characters must survive a parse_url() round trip because
    # some services distinguish literal characters from encoded ones.
    url = "https://example.com/a:b@c!d$e&f'g(h)i*j+k=n/file.txt"
    result = utils.parse.parse_url(url)
    assert result["fullpath"] == "/a:b@c!d$e&f'g(h)i*j+k=n/file.txt"
    assert result["url"] == url

    # Keep commas and semicolons encoded because Apprise uses them as path/list
    # delimiters. Otherwise path data such as a Home Assistant target list
    # could become extra segments. Related regressions have dedicated plugin
    # tests.
    result = utils.parse.parse_url("https://example.com/a,b;c/file.txt")
    assert result["fullpath"] == "/a%2Cb%3Bc/file.txt"

    # Characters outside an RFC 3986 path segment must remain encoded when the
    # safe list is expanded.
    result = utils.parse.parse_url("https://example.com/a b/c[1]#/file.txt")
    assert "%20" in result["fullpath"]
    assert "[" not in result["fullpath"]
    assert "]" not in result["fullpath"]


def test_parse_url_markdown_content():
    """
    utils: parse_url() with unmatched-bracket content (GH#1693)
    """
    # Content whose host portion carries an unmatched '[' (for example from
    # Markdown link syntax) makes the underlying urlparse() raise
    # "Invalid IPv6 URL". parse_url() documents that it returns None when
    # content cannot be extracted, so that ValueError must not propagate.
    assert (
        utils.parse.parse_url("**bold** and [link](https://example.com)")
        is None
    )
    assert utils.parse.parse_url("text with [unclosed bracket") is None


def test_parse_url_path_no_redos():
    "utils: parse_url() does not regress into slow/backtracking behavior"

    # Escaping uses linear-time table lookups. This guards against a future
    # regex-based implementation backtracking on untrusted paths.
    segment = "%40%3A%21%24%26%27%28%29%2A%2B%2C%3B%3D-a" * 5000
    url = f"https://example.com/{segment}/file.txt?foo=bar"

    start = time.time()
    result = utils.parse.parse_url(url)
    elapsed = time.time() - start

    assert result is not None
    # The generous limit allows slower CI while catching nonlinear behavior.
    assert elapsed < 5.0


def test_parse_url_simple():
    "utils: parse_url() testing"

    result = utils.parse.parse_url("http://hostname", simple=True)
    assert len(result) == 3
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["url"] == "http://hostname"

    result = utils.parse.parse_url("http://hostname/", simple=True)
    assert len(result) == 5
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["url"] == "http://hostname/"

    # colon after hostname without port number is no good
    assert utils.parse.parse_url("http://hostname:", simple=True) is None

    # An invalid port
    result = utils.parse.parse_url(
        "http://hostname:invalid",
        verify_host=False,
        strict_port=True,
        simple=True,
    )
    assert len(result) == 4
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == "invalid"
    assert result["url"] == "http://hostname:invalid"

    # However if we don't verify the host, it is okay
    result = utils.parse.parse_url(
        "http://hostname:", verify_host=False, simple=True
    )
    assert len(result) == 3
    assert result["schema"] == "http"
    assert result["host"] == "hostname:"
    assert result["url"] == "http://hostname:"

    # A port of Zero is not valid with strict port checking
    assert (
        utils.parse.parse_url(
            "http://hostname:0", strict_port=True, simple=True
        )
        is None
    )

    # Without strict port checking however, it is okay
    result = utils.parse.parse_url(
        "http://hostname:0", strict_port=False, simple=True
    )
    assert len(result) == 4
    assert result["schema"] == "http"
    assert result["port"] == 0
    assert result["host"] == "hostname"
    assert result["url"] == "http://hostname:0"

    # A negative port is not valid
    assert (
        utils.parse.parse_url(
            "http://hostname:-92", strict_port=True, simple=True
        )
        is None
    )
    result = utils.parse.parse_url(
        "http://hostname:-92", verify_host=False, strict_port=True, simple=True
    )
    assert len(result) == 4
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == -92
    assert result["url"] == "http://hostname:-92"

    # A port that is too large is not valid
    assert (
        utils.parse.parse_url(
            "http://hostname:65536", strict_port=True, simple=True
        )
        is None
    )

    # This is an accetable port (the maximum)
    result = utils.parse.parse_url(
        "http://hostname:65535", strict_port=True, simple=True
    )
    assert len(result) == 4
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == 65535
    assert result["url"] == "http://hostname:65535"

    # This is an accetable port (the maximum)
    result = utils.parse.parse_url(
        "http://hostname:1", strict_port=True, simple=True
    )
    assert len(result) == 4
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == 1
    assert result["url"] == "http://hostname:1"

    # A port that was identfied as a string is invalid
    assert (
        utils.parse.parse_url(
            "http://hostname:invalid", strict_port=True, simple=True
        )
        is None
    )
    result = utils.parse.parse_url(
        "http://hostname:invalid",
        verify_host=False,
        strict_port=True,
        simple=True,
    )
    assert len(result) == 4
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == "invalid"
    assert result["url"] == "http://hostname:invalid"

    result = utils.parse.parse_url(
        "http://hostname:invalid",
        verify_host=False,
        strict_port=False,
        simple=True,
    )
    assert len(result) == 3
    assert result["schema"] == "http"
    assert result["host"] == "hostname:invalid"
    assert result["url"] == "http://hostname:invalid"

    result = utils.parse.parse_url(
        "http://hostname:invalid?key=value&-minuskey=mvalue",
        verify_host=False,
        strict_port=False,
        simple=True,
    )
    assert len(result) == 4
    assert result["schema"] == "http"
    assert result["host"] == "hostname:invalid"
    assert result["url"] == "http://hostname:invalid"
    assert isinstance(result["qsd"], dict)
    assert len(result["qsd"]) == 2
    assert unquote(result["qsd"]["-minuskey"]) == "mvalue"
    assert unquote(result["qsd"]["key"]) == "value"

    # Handling of floats
    assert (
        utils.parse.parse_url(
            "http://hostname:4.2", strict_port=True, simple=True
        )
        is None
    )
    result = utils.parse.parse_url(
        "http://hostname:4.2", verify_host=False, strict_port=True, simple=True
    )
    assert len(result) == 4
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == "4.2"
    assert result["url"] == "http://hostname:4.2"

    # A Port of zero is not acceptable for a regular hostname
    assert (
        utils.parse.parse_url(
            "http://hostname:0", strict_port=True, simple=True
        )
        is None
    )

    # No host verification (zero is an acceptable port when this is the case
    result = utils.parse.parse_url(
        "http://hostname:0", verify_host=False, simple=True
    )
    assert len(result) == 4
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == 0
    assert result["url"] == "http://hostname:0"

    result = utils.parse.parse_url(
        "http://[2001:db8:002a:3256:adfe:05c0:0003:0006]:8080",
        verify_host=False,
        simple=True,
    )
    assert len(result) == 4
    assert result["schema"] == "http"
    assert result["host"] == "[2001:db8:002a:3256:adfe:05c0:0003:0006]"
    assert result["port"] == 8080
    assert (
        result["url"] == "http://[2001:db8:002a:3256:adfe:05c0:0003:0006]:8080"
    )

    result = utils.parse.parse_url(
        "http://hostname:0", verify_host=False, strict_port=True, simple=True
    )
    assert len(result) == 4
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == 0
    assert result["url"] == "http://hostname:0"

    result = utils.parse.parse_url("http://hostname/?-KeY=Value", simple=True)
    assert len(result) == 6
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["url"] == "http://hostname/"
    assert "-key" in result["qsd"]
    assert unquote(result["qsd"]["-key"]) == "Value"

    result = utils.parse.parse_url("http://hostname/?+KeY=Value", simple=True)
    assert len(result) == 6
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["url"] == "http://hostname/"
    assert "+key" in result["qsd"]
    assert result["qsd"]["+key"] == "Value"

    result = utils.parse.parse_url("http://hostname/?:kEy=vALUE", simple=True)
    assert len(result) == 6
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["url"] == "http://hostname/"
    assert ":key" in result["qsd"]
    assert result["qsd"][":key"] == "vALUE"

    result = utils.parse.parse_url(
        "http://hostname/?+KeY=ValueA&-kEy=ValueB&KEY=Value%20+C&:colon=y",
        simple=True,
    )
    assert len(result) == 6
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["url"] == "http://hostname/"
    assert "+key" in result["qsd"]
    assert "-key" in result["qsd"]
    assert ":colon" in result["qsd"]
    assert result["qsd"][":colon"] == "y"
    assert result["qsd"]["key"] == "Value +C"
    assert result["qsd"]["+key"] == "ValueA"
    assert result["qsd"]["-key"] == "ValueB"

    result = utils.parse.parse_url("http://hostname////", simple=True)
    assert len(result) == 5
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["url"] == "http://hostname/"

    result = utils.parse.parse_url("http://hostname:40////", simple=True)
    assert len(result) == 6
    assert result["schema"] == "http"
    assert result["host"] == "hostname"
    assert result["port"] == 40
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["url"] == "http://hostname:40/"

    result = utils.parse.parse_url("HTTP://HoStNaMe:40/test.php", simple=True)
    assert len(result) == 7
    assert result["schema"] == "http"
    assert result["host"] == "HoStNaMe"
    assert result["port"] == 40
    assert result["fullpath"] == "/test.php"
    assert result["path"] == "/"
    assert result["query"] == "test.php"
    assert result["url"] == "http://HoStNaMe:40/test.php"

    result = utils.parse.parse_url(
        "HTTPS://user@hostname/test.py", simple=True
    )
    assert len(result) == 7
    assert result["schema"] == "https"
    assert result["host"] == "hostname"
    assert result["user"] == "user"
    assert result["fullpath"] == "/test.py"
    assert result["path"] == "/"
    assert result["query"] == "test.py"
    assert result["url"] == "https://user@hostname/test.py"

    result = utils.parse.parse_url(
        "  HTTPS://///user@@@hostname///test.py  ", simple=True
    )
    assert len(result) == 7
    assert result["schema"] == "https"
    assert result["host"] == "hostname"
    assert result["user"] == "user"
    assert result["fullpath"] == "/test.py"
    assert result["path"] == "/"
    assert result["query"] == "test.py"
    assert result["url"] == "https://user@hostname/test.py"

    result = utils.parse.parse_url(
        "HTTPS://user:password@otherHost/full///path/name/",
        simple=True,
    )
    assert len(result) == 7
    assert result["schema"] == "https"
    assert result["host"] == "otherHost"
    assert result["user"] == "user"
    assert result["password"] == "password"
    assert result["fullpath"] == "/full/path/name/"
    assert result["path"] == "/full/path/name/"
    assert result["url"] == "https://user:password@otherHost/full/path/name/"

    # Handle garbage
    assert utils.parse.parse_url(None) is None

    result = utils.parse.parse_url(
        "mailto://user:password@otherHost/lead2gold@gmail.com"
        + "?from=test@test.com&name=Chris%20Caron&format=text",
        simple=True,
    )
    assert len(result) == 9
    assert result["schema"] == "mailto"
    assert result["host"] == "otherHost"
    assert result["user"] == "user"
    assert result["password"] == "password"
    assert unquote(result["fullpath"]) == "/lead2gold@gmail.com"
    assert result["path"] == "/"
    assert unquote(result["query"]) == "lead2gold@gmail.com"
    assert (
        unquote(result["url"])
        == "mailto://user:password@otherHost/lead2gold@gmail.com"
    )
    assert len(result["qsd"]) == 3
    assert "name" in result["qsd"]
    assert unquote(result["qsd"]["name"]) == "Chris Caron"
    assert "from" in result["qsd"]
    assert unquote(result["qsd"]["from"]) == "test@test.com"
    assert "format" in result["qsd"]
    assert unquote(result["qsd"]["format"]) == "text"

    # Test Passwords with question marks ?; not supported
    result = utils.parse.parse_url(
        "http://user:pass.with.?question@host", simple=True
    )
    assert result is None

    # just hostnames
    result = utils.parse.parse_url(
        "nuxref.com",
        simple=True,
    )
    assert len(result) == 3
    assert result["schema"] == "http"
    assert result["host"] == "nuxref.com"
    assert result["url"] == "http://nuxref.com"

    # just host and path
    result = utils.parse.parse_url("invalid/host", simple=True)
    assert len(result) == 6
    assert result["schema"] == "http"
    assert result["host"] == "invalid"
    assert result["fullpath"] == "/host"
    assert result["path"] == "/"
    assert result["query"] == "host"
    assert result["url"] == "http://invalid/host"

    # just all out invalid
    assert utils.parse.parse_url("?", simple=True) is None
    assert utils.parse.parse_url("/", simple=True) is None

    # Test some illegal strings
    result = utils.parse.parse_url(object, verify_host=False, simple=True)
    assert result is None
    result = utils.parse.parse_url(None, verify_host=False, simple=True)
    assert result is None

    # Just a schema; invalid host
    result = utils.parse.parse_url("test://", simple=True)
    assert result is None

    # Do it again without host validation
    result = utils.parse.parse_url("test://", verify_host=False, simple=True)
    assert len(result) == 2
    assert result["schema"] == "test"
    assert result["url"] == "test://"

    result = utils.parse.parse_url("testhostname", simple=True)
    assert len(result) == 3
    assert result["schema"] == "http"
    assert result["host"] == "testhostname"
    # The default_schema kicks in here
    assert result["url"] == "http://testhostname"

    result = utils.parse.parse_url(
        "example.com", default_schema="unknown", simple=True
    )
    assert len(result) == 3
    assert result["schema"] == "unknown"
    assert result["host"] == "example.com"
    # The default_schema kicks in here
    assert result["url"] == "unknown://example.com"

    # An empty string without a hostame is still valid if verify_host is set
    result = utils.parse.parse_url("", verify_host=False, simple=True)
    assert len(result) == 2
    assert result["schema"] == "http"
    # The default_schema kicks in here
    assert result["url"] == "http://"

    # A messed up URL
    result = utils.parse.parse_url(
        "test://:@/", verify_host=False, simple=True
    )
    assert len(result) == 6
    assert result["schema"] == "test"
    assert result["user"] == ""
    assert result["password"] == ""
    assert result["fullpath"] == "/"
    assert result["path"] == "/"
    assert result["url"] == "test://:@/"

    result = utils.parse.parse_url(
        "crazy://:@//_/@^&/jack.json", verify_host=False, simple=True
    )
    assert len(result) == 7
    assert result["schema"] == "crazy"
    assert result["user"] == ""
    assert result["password"] == ""
    assert unquote(result["fullpath"]) == "/_/@^&/jack.json"
    assert unquote(result["path"]) == "/_/@^&/"
    assert result["query"] == "jack.json"
    assert unquote(result["url"]) == "crazy://:@/_/@^&/jack.json"


def test_url_assembly():
    """ "utils: url_assembly() testing."""

    url = "schema://user:password@hostname:port/path/?key=value"
    assert (
        utils.parse.url_assembly(
            **utils.parse.parse_url(url, verify_host=False)
        )
        == url
    )

    # Same URL without trailing slash after path
    url = "schema://user:password@hostname:port/path?key=value"
    assert (
        utils.parse.url_assembly(
            **utils.parse.parse_url(url, verify_host=False)
        )
        == url
    )

    url = "schema://user@hostname:port/path?key=value"
    assert (
        utils.parse.url_assembly(
            **utils.parse.parse_url(url, verify_host=False)
        )
        == url
    )

    url = "schema://hostname:10/a/file.php"
    assert (
        utils.parse.url_assembly(
            **utils.parse.parse_url(url, verify_host=False)
        )
        == url
    )

    # When spaces and special characters are introduced, the URL
    # is hard to mimic what was entered. Instead it is normalized
    url = (
        "schema://hostname:10/a space/file.php?"
        "arg=a+space&arg2=a%20space&arg3=a space"
    )
    assert (
        utils.parse.url_assembly(
            **utils.parse.parse_url(url, verify_host=False)
        )
        == "schema://hostname:10/a%20space/file.php?"
        "arg=a%2Bspace&arg2=a%20space&arg3=a%20space"
    )

    # encode=True should only be used if you're passing in un-assembled
    # content... hence the following is likely not what is expected:
    assert (
        utils.parse.url_assembly(
            **utils.parse.parse_url(url, verify_host=False), encode=True
        )
        == "schema://hostname:10/a%2520space/file.php?"
        "arg=a%2Bspace&arg2=a%20space&arg3=a%20space"
    )

    # But the following utilizes the encode=True and produces the
    # desired effects:
    content = {
        "host": "hostname",
        # Note that fullpath requires escaping in this case
        "fullpath": "/a space/file.php",
        "path": "/a space/",
        "query": "file.php",
        "schema": "schema",
        # our query arguments also require escaping as well
        "qsd": {"arg": "a+space", "arg2": "a space", "arg3": "a space"},
    }
    assert (
        utils.parse.url_assembly(**content, encode=True)
        == "schema://hostname/a%20space/file.php?"
        "arg=a%2Bspace&arg2=a%20space&arg3=a%20space"
    )


def test_parse_bool():
    "utils: parse_bool() testing"

    assert utils.parse.parse_bool("Enabled", None) is True
    assert utils.parse.parse_bool("Disabled", None) is False
    assert utils.parse.parse_bool("Allow", None) is True
    assert utils.parse.parse_bool("Deny", None) is False
    assert utils.parse.parse_bool("Yes", None) is True
    assert utils.parse.parse_bool("YES", None) is True
    assert utils.parse.parse_bool("Always", None) is True
    assert utils.parse.parse_bool("No", None) is False
    assert utils.parse.parse_bool("NO", None) is False
    assert utils.parse.parse_bool("NEVER", None) is False
    assert utils.parse.parse_bool("TrUE", None) is True
    assert utils.parse.parse_bool("tRUe", None) is True
    assert utils.parse.parse_bool("FAlse", None) is False
    assert utils.parse.parse_bool("F", None) is False
    assert utils.parse.parse_bool("T", None) is True
    assert utils.parse.parse_bool("0", None) is False
    assert utils.parse.parse_bool("1", None) is True
    assert utils.parse.parse_bool("True", None) is True
    assert utils.parse.parse_bool("Yes", None) is True
    assert utils.parse.parse_bool(1, None) is True
    assert utils.parse.parse_bool(0, None) is False
    assert utils.parse.parse_bool(True, None) is True
    assert utils.parse.parse_bool(False, None) is False

    # only the int of 0 will return False since the function
    # casts this to a boolean
    assert utils.parse.parse_bool(2, None) is True
    # An empty list is still false
    assert utils.parse.parse_bool([], None) is False
    # But a list that contains something is True
    assert (
        utils.parse.parse_bool(
            [
                "value",
            ],
            None,
        )
        is True
    )

    # Use Default (which is False)
    assert utils.parse.parse_bool("OhYeah") is False
    # Adjust Default and get a different result
    assert utils.parse.parse_bool("OhYeah", True) is True


def test_is_uuid():
    """
    API: is_uuid() function
    """
    # Invalid Entries
    assert utils.parse.is_uuid("invalid") is False
    assert utils.parse.is_uuid(None) is False
    assert utils.parse.is_uuid(5) is False
    assert utils.parse.is_uuid(object) is False

    # A slightly invalid uuid4 entry
    assert utils.parse.is_uuid("591ed387-fa65-ac97-9712-b9d2a15e42a9") is False
    assert utils.parse.is_uuid("591ed387-fa65-Jc97-9712-b9d2a15e42a9") is False

    # Valid UUID4 Entries
    assert utils.parse.is_uuid("591ed387-fa65-4c97-9712-b9d2a15e42a9") is True
    assert utils.parse.is_uuid("32b0b447-fe84-4df1-8368-81925e729265") is True


def test_is_hostname():
    """
    API: is_hostname() function

    """
    # Valid Hostnames
    assert utils.parse.is_hostname("yahoo.ca") == "yahoo.ca"
    assert utils.parse.is_hostname("yahoo.ca.") == "yahoo.ca"
    assert (
        utils.parse.is_hostname("valid-dashes-in-host.ca")
        == "valid-dashes-in-host.ca"
    )
    assert (
        utils.parse.is_hostname("valid-underscores_in_host.ca")
        == "valid-underscores_in_host.ca"
    )

    # Underscores are supported by default
    assert (
        utils.parse.is_hostname("valid_dashes_in_host.ca")
        == "valid_dashes_in_host.ca"
    )
    # However they are not if specified otherwise:
    assert (
        utils.parse.is_hostname("valid_dashes_in_host.ca", underscore=False)
        is False
    )

    # Invalid Hostnames
    assert (
        utils.parse.is_hostname("-hostname.that.starts.with.a.dash") is False
    )
    assert utils.parse.is_hostname("invalid-characters_#^.ca") is False
    assert utils.parse.is_hostname("    spaces   ") is False
    assert utils.parse.is_hostname("       ") is False
    assert utils.parse.is_hostname("") is False

    # Valid IPv4 Addresses
    assert utils.parse.is_hostname("127.0.0.1") == "127.0.0.1"
    assert utils.parse.is_hostname("0.0.0.0") == "0.0.0.0"
    assert utils.parse.is_hostname("255.255.255.255") == "255.255.255.255"

    # But not if we're not checking for this:
    assert utils.parse.is_hostname("127.0.0.1", ipv4=False) is False
    assert utils.parse.is_hostname("0.0.0.0", ipv4=False) is False
    assert utils.parse.is_hostname("255.255.255.255", ipv4=False) is False

    # Invalid IPv4 Addresses
    assert utils.parse.is_hostname("1.2.3") is False
    assert utils.parse.is_hostname("256.256.256.256") is False
    assert utils.parse.is_hostname("999.0.0.0") is False
    assert utils.parse.is_hostname("1.2.3.4.5") is False
    assert utils.parse.is_hostname("    127.0.0.1   ") is False
    assert utils.parse.is_hostname("       ") is False
    assert utils.parse.is_hostname("") is False

    # Valid IPv6 Addresses (square brakets supported for URL construction)
    assert (
        utils.parse.is_hostname("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]")
        == "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"
    )
    assert (
        utils.parse.is_hostname("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
        == "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"
    )
    assert (
        utils.parse.is_hostname("[2001:db8:002a:3256:adfe:05c0:0003:0006]")
        == "[2001:db8:002a:3256:adfe:05c0:0003:0006]"
    )

    # localhost
    assert utils.parse.is_hostname("::1") == "[::1]"
    assert utils.parse.is_hostname("0:0:0:0:0:0:0:1") == "[0:0:0:0:0:0:0:1]"

    # But not if we're not checking for this:
    assert (
        utils.parse.is_hostname(
            "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", ipv6=False
        )
        is False
    )
    assert (
        utils.parse.is_hostname(
            "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ipv6=False
        )
        is False
    )

    # Test hostnames with a single character hostname
    assert (
        utils.parse.is_hostname("cloud.a.example.com", ipv4=False, ipv6=False)
        == "cloud.a.example.com"
    )


def test_is_ipaddr():
    """
    API: is_ipaddr() function

    """
    # Valid IPv4 Addresses
    assert utils.parse.is_ipaddr("127.0.0.1") == "127.0.0.1"
    assert utils.parse.is_ipaddr("0.0.0.0") == "0.0.0.0"
    assert utils.parse.is_ipaddr("255.255.255.255") == "255.255.255.255"

    # Invalid IPv4 Addresses
    assert utils.parse.is_ipaddr("1.2.3") is False
    assert utils.parse.is_ipaddr("256.256.256.256") is False
    assert utils.parse.is_ipaddr("999.0.0.0") is False
    assert utils.parse.is_ipaddr("1.2.3.4.5") is False
    assert utils.parse.is_ipaddr("    127.0.0.1   ") is False
    assert utils.parse.is_ipaddr("       ") is False
    assert utils.parse.is_ipaddr("") is False

    # Valid IPv6 Addresses (square brakets supported for URL construction)
    assert (
        utils.parse.is_ipaddr("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]")
        == "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"
    )
    assert (
        utils.parse.is_ipaddr("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
        == "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"
    )
    assert (
        utils.parse.is_ipaddr("[2001:db8:002a:3256:adfe:05c0:0003:0006]")
        == "[2001:db8:002a:3256:adfe:05c0:0003:0006]"
    )

    # localhost
    assert utils.parse.is_ipaddr("::1") == "[::1]"
    assert utils.parse.is_ipaddr("0:0:0:0:0:0:0:1") == "[0:0:0:0:0:0:0:1]"


def test_is_email():
    """
    API: is_email() function

    """
    # Valid Emails
    results = utils.parse.is_email("test@gmail.com")
    assert results["name"] == ""
    assert results["email"] == "test@gmail.com"
    assert results["full_email"] == "test@gmail.com"
    assert results["domain"] == "gmail.com"
    assert results["user"] == "test"
    assert results["label"] == ""

    results = utils.parse.is_email("test@my-valid_host.com")
    assert results["name"] == ""
    assert results["email"] == "test@my-valid_host.com"
    assert results["full_email"] == "test@my-valid_host.com"
    assert results["domain"] == "my-valid_host.com"
    assert results["user"] == "test"
    assert results["label"] == ""

    results = utils.parse.is_email("tag+test@gmail.com")
    assert results["name"] == ""
    assert results["email"] == "test@gmail.com"
    assert results["full_email"] == "tag+test@gmail.com"
    assert results["domain"] == "gmail.com"
    assert results["user"] == "test"
    assert results["label"] == "tag"

    # Support Full Names as well
    results = utils.parse.is_email("Bill Gates: bgates@microsoft.com")
    assert results["name"] == "Bill Gates"
    assert results["email"] == "bgates@microsoft.com"
    assert results["full_email"] == "bgates@microsoft.com"
    assert results["domain"] == "microsoft.com"
    assert results["user"] == "bgates"
    assert results["label"] == ""

    results = utils.parse.is_email("Bill Gates <bgates@microsoft.com>")
    assert results["name"] == "Bill Gates"
    assert results["email"] == "bgates@microsoft.com"
    assert results["full_email"] == "bgates@microsoft.com"
    assert results["domain"] == "microsoft.com"
    assert results["user"] == "bgates"
    assert results["label"] == ""

    results = utils.parse.is_email("Bill Gates: <bgates@microsoft.com>")
    assert results["name"] == "Bill Gates"
    assert results["email"] == "bgates@microsoft.com"
    assert results["full_email"] == "bgates@microsoft.com"
    assert results["domain"] == "microsoft.com"
    assert results["user"] == "bgates"
    assert results["label"] == ""

    results = utils.parse.is_email("Sundar Pichai <ceo+spichai@gmail.com>")
    assert results["name"] == "Sundar Pichai"
    assert results["email"] == "spichai@gmail.com"
    assert results["full_email"] == "ceo+spichai@gmail.com"
    assert results["domain"] == "gmail.com"
    assert results["user"] == "spichai"
    assert results["label"] == "ceo"

    # Support Quotes
    results = utils.parse.is_email('"Chris Hemsworth" <ch@test.com>')
    assert results["name"] == "Chris Hemsworth"
    assert results["email"] == "ch@test.com"
    assert results["full_email"] == "ch@test.com"
    assert results["domain"] == "test.com"
    assert results["user"] == "ch"
    assert results["label"] == ""

    # An email without name, but contains delimiters
    results = utils.parse.is_email("      <spichai@gmail.com>")
    assert results["name"] == ""
    assert results["email"] == "spichai@gmail.com"
    assert results["full_email"] == "spichai@gmail.com"
    assert results["domain"] == "gmail.com"
    assert results["user"] == "spichai"
    assert results["label"] == ""

    # a valid email not properly delimited with a colon or angle bracket
    # We do a best guess and still parse it correctly
    results = utils.parse.is_email("Name valid@example.com")
    assert results["name"] == "Name"
    assert results["email"] == "valid@example.com"
    assert results["full_email"] == "valid@example.com"
    assert results["domain"] == "example.com"
    assert results["user"] == "valid"
    assert results["label"] == ""

    # a valid email not properly delimited with a colon or angle bracket
    # We do a best guess and still parse it correctly
    results = utils.parse.is_email("Руслан Эра russian+russia@example.ru")
    assert results["name"] == "Руслан Эра"
    assert results["email"] == "russia@example.ru"
    assert results["full_email"] == "russian+russia@example.ru"
    assert results["domain"] == "example.ru"
    assert results["user"] == "russia"
    assert results["label"] == "russian"

    # Invalid Emails
    assert utils.parse.is_email("invalid.com") is False
    assert utils.parse.is_email(object()) is False
    assert utils.parse.is_email(None) is False
    assert utils.parse.is_email("Just A Name") is False
    assert utils.parse.is_email("Name <bademail>") is False

    # Extended valid emails
    #
    # The first + denotes our label, so this test really validates
    # that there is a correct split and parsing of our email
    results = utils.parse.is_email("a-z0-9_!#$%&*+/=?%`{|}~^.-@gmail.com")
    assert results["name"] == ""
    assert results["label"] == "a-z0-9_!#$%&*"
    assert results["email"] == "/=?%`{|}~^.-@gmail.com"
    assert results["full_email"] == "a-z0-9_!#$%&*+/=?%`{|}~^.-@gmail.com"
    assert results["domain"] == "gmail.com"
    assert results["user"] == "/=?%`{|}~^.-"

    # A similar test without '+' (use of a label)

    # The first + denotes our label, so this test really validates
    # that there is a correct split and parsing of our email
    results = utils.parse.is_email("a-z0-9_!#$%&*/=?%`{|}~^.-@gmail.com")
    assert results["name"] == ""
    assert results["label"] == ""
    assert results["email"] == "a-z0-9_!#$%&*/=?%`{|}~^.-@gmail.com"
    assert results["full_email"] == "a-z0-9_!#$%&*/=?%`{|}~^.-@gmail.com"
    assert results["domain"] == "gmail.com"
    assert results["user"] == "a-z0-9_!#$%&*/=?%`{|}~^.-"


def test_is_domain_service_target():
    """
    API: is_domain_service_target() function

    """
    # Invalid information
    assert utils.parse.is_domain_service_target(None) is False
    assert utils.parse.is_domain_service_target(42) is False
    assert utils.parse.is_domain_service_target(object) is False
    assert utils.parse.is_domain_service_target("") is False
    assert utils.parse.is_domain_service_target("+()") is False
    assert utils.parse.is_domain_service_target("+") is False

    # Valid entries
    result = utils.parse.is_domain_service_target("service")
    assert isinstance(result, dict)
    assert result["service"] == "service"
    # Default domain
    assert result["domain"] == "notify"
    assert isinstance(result["targets"], list)
    assert len(result["targets"]) == 0

    result = utils.parse.is_domain_service_target("domain.service")
    assert isinstance(result, dict)
    assert result["service"] == "service"
    assert result["domain"] == "domain"
    assert isinstance(result["targets"], list)
    assert len(result["targets"]) == 0

    result = utils.parse.is_domain_service_target("domain.service:target")
    assert isinstance(result, dict)
    assert result["service"] == "service"
    assert result["domain"] == "domain"
    assert isinstance(result["targets"], list)
    assert len(result["targets"]) == 1
    assert result["targets"][0] == "target"

    result = utils.parse.is_domain_service_target("domain.service:t1,t2,t3")
    assert isinstance(result, dict)
    assert result["service"] == "service"
    assert result["domain"] == "domain"
    assert isinstance(result["targets"], list)
    assert len(result["targets"]) == 3
    assert "t1" in result["targets"]
    assert "t2" in result["targets"]
    assert "t3" in result["targets"]

    result = utils.parse.is_domain_service_target(
        "service:t1,t2,t3", domain="new_default"
    )
    assert isinstance(result, dict)
    assert result["service"] == "service"
    # Default domain
    assert result["domain"] == "new_default"
    assert isinstance(result["targets"], list)
    assert len(result["targets"]) == 3
    assert "t1" in result["targets"]
    assert "t2" in result["targets"]
    assert "t3" in result["targets"]


def test_is_call_sign_no():
    """
    API: is_call_sign() function

    """
    # Invalid numbers
    assert utils.parse.is_call_sign(None) is False
    assert utils.parse.is_call_sign(42) is False
    assert utils.parse.is_call_sign(object) is False
    assert utils.parse.is_call_sign("") is False
    assert utils.parse.is_call_sign("1") is False
    assert utils.parse.is_call_sign("12") is False
    assert utils.parse.is_call_sign("abc") is False
    assert utils.parse.is_call_sign("+()") is False
    assert utils.parse.is_call_sign("+") is False
    assert utils.parse.is_call_sign(None) is False
    assert utils.parse.is_call_sign(42) is False

    # To short or 2 long
    assert utils.parse.is_call_sign("DF1ABCX") is False
    assert utils.parse.is_call_sign("DF1ABCEFG") is False
    assert utils.parse.is_call_sign("1ABCX") is False
    # 4th character is not an number
    assert utils.parse.is_call_sign("XXXXXX") is False

    # Some valid checks
    # 1x2
    result = utils.parse.is_call_sign("A0AF")
    assert isinstance(result, dict)
    assert result["callsign"] == "A0AF"
    assert result["ssid"] == ""

    # 2x1
    result = utils.parse.is_call_sign("AA0A")
    assert isinstance(result, dict)
    assert result["callsign"] == "AA0A"
    assert result["ssid"] == ""

    # 2x2
    result = utils.parse.is_call_sign("AA0AF")
    assert isinstance(result, dict)
    assert result["callsign"] == "AA0AF"
    assert result["ssid"] == ""

    result = utils.parse.is_call_sign("AA0AF-23")
    assert isinstance(result, dict)
    assert result["callsign"] == "AA0AF"
    assert result["ssid"] == "23"

    # 1x3
    result = utils.parse.is_call_sign("K0ACL")
    assert isinstance(result, dict)
    assert result["callsign"] == "K0ACL"
    assert result["ssid"] == ""

    # 2x3
    result = utils.parse.is_call_sign("DF1ABC")
    assert isinstance(result, dict)
    assert result["callsign"] == "DF1ABC"
    assert result["ssid"] == ""

    # Get our SSID
    result = utils.parse.is_call_sign("DF1ABC-14")
    assert result["callsign"] == "DF1ABC"
    assert result["ssid"] == "14"


def test_is_phone_no():
    """
    API: is_phone_no() function

    """
    # Invalid numbers
    assert utils.parse.is_phone_no(None) is False
    assert utils.parse.is_phone_no(42) is False
    assert utils.parse.is_phone_no(object) is False
    assert utils.parse.is_phone_no("") is False
    assert utils.parse.is_phone_no("1") is False
    assert utils.parse.is_phone_no("12") is False
    assert utils.parse.is_phone_no("abc") is False
    assert utils.parse.is_phone_no("+()") is False
    assert utils.parse.is_phone_no("+") is False
    assert utils.parse.is_phone_no(None) is False
    assert utils.parse.is_phone_no(42) is False
    assert utils.parse.is_phone_no(object, min_len=0) is False
    assert utils.parse.is_phone_no("", min_len=1) is False
    assert utils.parse.is_phone_no("abc", min_len=0) is False
    assert utils.parse.is_phone_no("", min_len=0) is False

    # Ambigious, but will document it here in this test as such
    results = utils.parse.is_phone_no("+((()))--+", min_len=0)
    assert results["country"] == ""
    assert results["area"] == ""
    assert results["line"] == ""
    assert results["pretty"] == ""
    assert results["full"] == ""

    # Valid phone numbers
    assert utils.parse.is_phone_no("+(0)") is False
    results = utils.parse.is_phone_no("+(0)", min_len=1)
    assert results["country"] == ""
    assert results["area"] == ""
    assert results["line"] == "0"
    assert results["pretty"] == "0"
    assert results["full"] == "0"

    assert utils.parse.is_phone_no("1") is False
    results = utils.parse.is_phone_no("1", min_len=1)
    assert results["country"] == ""
    assert results["area"] == ""
    assert results["line"] == "1"
    assert results["pretty"] == "1"
    assert results["full"] == "1"

    assert utils.parse.is_phone_no("12") is False
    results = utils.parse.is_phone_no("12", min_len=2)
    assert results["country"] == ""
    assert results["area"] == ""
    assert results["line"] == "12"
    assert results["pretty"] == "12"
    assert results["full"] == "12"

    assert utils.parse.is_phone_no("911") is False
    results = utils.parse.is_phone_no("911", min_len=3)
    assert isinstance(results, dict)
    assert results["country"] == ""
    assert results["area"] == ""
    assert results["line"] == "911"
    assert results["pretty"] == "911"
    assert results["full"] == "911"

    assert utils.parse.is_phone_no("1234") is False
    results = utils.parse.is_phone_no("1234", min_len=4)
    assert isinstance(results, dict)
    assert results["country"] == ""
    assert results["area"] == ""
    assert results["line"] == "1234"
    assert results["pretty"] == "1234"
    assert results["full"] == "1234"

    assert utils.parse.is_phone_no("12345") is False
    results = utils.parse.is_phone_no("12345", min_len=5)
    assert isinstance(results, dict)
    assert results["country"] == ""
    assert results["area"] == ""
    assert results["line"] == "12345"
    assert results["pretty"] == "12345"
    assert results["full"] == "12345"

    assert utils.parse.is_phone_no("123456") is False
    results = utils.parse.is_phone_no("123456", min_len=6)
    assert isinstance(results, dict)
    assert results["country"] == ""
    assert results["area"] == ""
    assert results["line"] == "123456"
    assert results["pretty"] == "123456"
    assert results["full"] == "123456"

    # at 7 digits, the format hyphenates in the `pretty` section
    assert utils.parse.is_phone_no("1234567") is False
    results = utils.parse.is_phone_no("1234567", min_len=7)
    assert isinstance(results, dict)
    assert results["country"] == ""
    assert results["area"] == ""
    assert results["line"] == "1234567"
    assert results["pretty"] == "123-4567"
    assert results["full"] == "1234567"

    results = utils.parse.is_phone_no("1(800) 123-4567")
    assert isinstance(results, dict)
    assert results["country"] == "1"
    assert results["area"] == "800"
    assert results["line"] == "1234567"
    assert results["pretty"] == "+1 800-123-4567"
    assert results["full"] == "18001234567"


def test_parse_call_sign():
    """utils: parse_call_sign() testing"""
    # A simple single array entry (As str)
    results = utils.parse.parse_call_sign("")
    assert isinstance(results, list)
    assert len(results) == 0

    # just delimeters
    results = utils.parse.parse_call_sign(",  ,, , ,,, ")
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_call_sign(None)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_call_sign(42)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_call_sign(
        "this is not a parseable call sign at all"
    )
    assert isinstance(results, list)
    assert len(results) == 9

    results = utils.parse.parse_call_sign(
        "this is not a parseable call sign at all", store_unparseable=False
    )
    assert isinstance(results, list)
    assert len(results) == 0

    # Now test valid call signs
    results = utils.parse.parse_call_sign("0A1DEF")
    assert isinstance(results, list)
    assert len(results) == 1
    assert "0A1DEF" in results

    results = utils.parse.parse_call_sign("0A1DEF, DF1ABC")
    assert isinstance(results, list)
    assert len(results) == 2
    assert "0A1DEF" in results
    assert "DF1ABC" in results

    results = utils.parse.parse_call_sign("AA0A, A0AF-12, GARBAGE")
    assert isinstance(results, list)
    assert len(results) == 2
    assert "AA0A" in results
    assert "A0AF-12" in results


def test_parse_domain_service_targets():
    """utils: parse_domain_service_targets() testing"""

    results = utils.parse.parse_domain_service_targets("")
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_domain_service_targets("service1 service2")
    assert isinstance(results, list)
    assert len(results) == 2
    assert "service1" in results
    assert "service2" in results

    results = utils.parse.parse_domain_service_targets(
        "service1:target1,target2"
    )
    assert isinstance(results, list)
    assert len(results) == 1
    assert "service1:target1,target2" in results

    results = utils.parse.parse_domain_service_targets(
        "service1:target1,target2 service2 domain.service3"
    )
    assert isinstance(results, list)
    assert len(results) == 3
    assert "service1:target1,target2" in results
    assert "service2" in results
    assert "domain.service3" in results

    # Support a comma in the space between entries
    results = utils.parse.parse_domain_service_targets(
        "service1:target1,target2, service2 ,domain.service3,    ,  , service4"
    )
    assert isinstance(results, list)
    assert len(results) == 4
    assert "service1:target1,target2" in results
    assert "service2" in results
    assert "domain.service3" in results
    assert "service4" in results

    results = utils.parse.parse_domain_service_targets(
        "service:target1,target2"
    )
    assert isinstance(results, list)
    assert len(results) == 1
    assert "service:target1,target2" in results

    # Handle unparseables
    results = utils.parse.parse_domain_service_targets(
        ": %invalid ^entries%", store_unparseable=False
    )
    assert isinstance(results, list)
    assert len(results) == 0

    # Verify many separators complete in reasonable time.
    t0 = time.monotonic()
    utils.parse.parse_domain_service_targets("ha.smtp" + " , " * 30)
    assert time.monotonic() - t0 < 5.0


def test_parse_phone_no():
    """utils: parse_phone_no() testing"""
    # A simple single array entry (As str)
    results = utils.parse.parse_phone_no("")
    assert isinstance(results, list)
    assert len(results) == 0

    # just delimeters
    results = utils.parse.parse_phone_no(",  ,, , ,,, ")
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_phone_no(",")
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_phone_no(None)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_phone_no(42)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_phone_no(
        "this is not a parseable phoneno at all"
    )
    assert isinstance(results, list)
    assert len(results) == 8
    # Now we do it again with the store_unparsable flag set to False
    results = utils.parse.parse_phone_no(
        "this is not a parseable email at all", store_unparseable=False
    )
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_phone_no("+", store_unparseable=False)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_phone_no("(", store_unparseable=False)
    assert isinstance(results, list)
    assert len(results) == 0

    # Number is too short
    results = utils.parse.parse_phone_no("0", store_unparseable=False)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_phone_no("12", store_unparseable=False)
    assert isinstance(results, list)
    assert len(results) == 0

    # Now test valid phone numbers
    results = utils.parse.parse_phone_no("+1 (124) 245 2345")
    assert isinstance(results, list)
    assert len(results) == 1
    assert "+1 (124) 245 2345" in results

    results = utils.parse.parse_phone_no("911", store_unparseable=False)
    assert isinstance(results, list)
    assert len(results) == 1
    assert "911" in results

    results = utils.parse.parse_phone_no(
        "911, 123-123-1234", store_unparseable=False
    )
    assert isinstance(results, list)
    assert len(results) == 2
    assert "911" in results
    assert "123-123-1234" in results

    # Space variations
    results = utils.parse.parse_phone_no(" 911  , +1 (123) 123-1234")
    assert isinstance(results, list)
    assert len(results) == 2
    assert "911" in results
    assert "+1 (123) 123-1234" in results

    results = utils.parse.parse_phone_no(" 911  , + 1 ( 123 ) 123-1234")
    assert isinstance(results, list)
    assert len(results) == 2
    assert "911" in results
    assert "+ 1 ( 123 ) 123-1234" in results

    # Verify trailing-whitespace runs complete in reasonable time.
    t0 = time.monotonic()
    results = utils.parse.parse_phone_no("+12345678901" + " " * 1000)
    assert time.monotonic() - t0 < 5.0
    assert results == ["+12345678901"]


def test_parse_emails():
    """utils: parse_emails() testing"""
    # A simple single array entry (As str)
    results = utils.parse.parse_emails("")
    assert isinstance(results, list)
    assert len(results) == 0

    # just delimeters
    results = utils.parse.parse_emails(",  ,, , ,,, ")
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_emails(",")
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_emails(None)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_emails(42)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_emails("this is not a parseable email at all")
    assert isinstance(results, list)
    assert len(results) == 8
    # Now we do it again with the store_unparsable flag set to False
    results = utils.parse.parse_emails(
        "this is not a parseable email at all", store_unparseable=False
    )
    assert isinstance(results, list)
    assert len(results) == 0

    # Now test valid emails
    results = utils.parse.parse_emails("user@example.com")
    assert isinstance(results, list)
    assert len(results) == 1
    assert "user@example.com" in results

    results = utils.parse.parse_emails("a@")
    assert isinstance(results, list)
    assert len(results) == 1
    assert "a@" in results

    results = utils.parse.parse_emails("user1@example.com user2@example.com")
    assert isinstance(results, list)
    assert len(results) == 2
    assert "user1@example.com" in results
    assert "user2@example.com" in results

    # Commas and spaces found inside URLs are ignored
    emails = [
        "user1@example.com,",
        "test1@example.com,,, abcd@example.com",
        "Chuck Norris roundhouse@kick.com",
        "David Spade dspade@example.com, Yours Truly yours@truly.com",
    ]

    results = utils.parse.parse_emails(", ".join(emails))
    assert isinstance(results, list)
    assert len(results) == 6
    assert "user1@example.com" in results
    assert "test1@example.com" in results
    assert "abcd@example.com" in results
    assert "Chuck Norris roundhouse@kick.com" in results
    assert "David Spade dspade@example.com" in results
    assert "Yours Truly yours@truly.com" in results

    # Test triangle bracket parsing
    # Commas and spaces found inside URLs are ignored
    emails = [
        "User1 user1@example.com",
        "User 2 user2@example.com",
        "User Three <user3@example.com>",
        "The Forth User: <user4@example.com>",
        "5th User: user4@example.com",
    ]

    results = utils.parse.parse_emails(", ".join(emails))
    assert isinstance(results, list)
    assert len(results) == len(emails)
    for email in emails:
        assert email in results
        is_email = utils.parse.is_email(email)
        assert is_email
        assert is_email.get("name")

    # pass the entries in as a list
    results = utils.parse.parse_emails(emails)
    assert isinstance(results, list)
    assert len(results) == len(emails)
    for email in emails:
        assert email in results

    # Pass in some unparseables
    results = utils.parse.parse_emails("garbage")
    assert isinstance(results, list)
    assert len(results) == 1

    results = utils.parse.parse_emails("garbage", store_unparseable=False)
    assert isinstance(results, list)
    assert len(results) == 0

    # Pass in garbage
    results = utils.parse.parse_emails(object)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_emails(42)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_emails([None, object, 42])
    assert isinstance(results, list)
    assert len(results) == 0

    # Verify trailing-whitespace runs complete in reasonable time.
    t0 = time.monotonic()
    results = utils.parse.parse_emails("victim@example.com" + " " * 400)
    assert time.monotonic() - t0 < 5.0
    assert results == ["victim@example.com"]

    # Quoted display names containing commas are kept as a single token.
    results = utils.parse.parse_emails(
        '"John, Jason & Mike" <team@example.com>'
    )
    assert results == ['"John, Jason & Mike" <team@example.com>']

    results = utils.parse.parse_emails(
        '"Smith, Jane" <jane@example.com>, Bob Jones <bob@example.com>'
    )
    assert len(results) == 2
    assert '"Smith, Jane" <jane@example.com>' in results
    assert "Bob Jones <bob@example.com>" in results


def test_parse_urls():
    """utils: parse_urls() testing"""
    # A simple single array entry (As str)
    results = utils.parse.parse_urls("")
    assert isinstance(results, list)
    assert len(results) == 0

    # just delimeters
    results = utils.parse.parse_urls(",  ,, , ,,, ")
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_urls(",")
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_urls(None)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_urls(42)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_urls("this is not a parseable url at all")
    assert isinstance(results, list)
    # we still end up returning this
    assert len(results) == 8

    results = utils.parse.parse_urls(
        "this is not a parseable url at all", store_unparseable=False
    )
    assert isinstance(results, list)
    assert len(results) == 0

    # Now test valid URLs
    results = utils.parse.parse_urls("windows://")
    assert isinstance(results, list)
    assert len(results) == 1
    assert "windows://" in results

    results = utils.parse.parse_urls("windows:// gnome://")
    assert isinstance(results, list)
    assert len(results) == 2
    assert "windows://" in results
    assert "gnome://" in results

    # We don't want to parse out URLs that are part of another URL's arguments
    results = utils.parse.parse_urls("discord://host?url=https://localhost")
    assert isinstance(results, list)
    assert len(results) == 1
    assert "discord://host?url=https://localhost" in results

    # Commas and spaces found inside URLs are ignored
    urls = [
        (
            "mailgun://noreply@sandbox.mailgun.org/apikey/"
            "?to=test@example.com,test2@example.com,,"
            " abcd@example.com"
        ),
        (
            "mailgun://noreply@sandbox.another.mailgun.org/apikey/"
            "?to=hello@example.com,,hmmm@example.com,, abcd@example.com, ,"
        ),
        "windows://",
    ]

    # Since comma's and whitespace are the delimiters; they won't be
    # present at the end of the URL; so we just need to write a special
    # rstrip() as a regular exression to handle whitespace (\s) and comma
    # delimiter
    rstrip_re = re.compile(r"[\s,]+$")

    # Since a comma acts as a delimiter, we run a risk of a problem where the
    # comma exists as part of the URL and is therefore lost if it was found
    # at the end of it.

    results = utils.parse.parse_urls(", ".join(urls))
    assert isinstance(results, list)
    assert len(results) == len(urls)
    for url in urls:
        assert rstrip_re.sub("", url) in results

    # However if a comma is found at the end of a single url without a new
    # match to hit, it is saved and not lost

    # The comma at the end of the password will not be lost if we're
    # dealing with a single entry:
    url = "http://hostname?password=,abcd,"
    results = utils.parse.parse_urls(url)
    assert isinstance(results, list)
    assert len(results) == 1
    assert url in results

    # however if we have multiple entries, commas and spaces between
    # URLs will be lost, however the last URL will not lose the comma
    urls = [
        "schema1://hostname?password=,abcd,",
        "schema2://hostname?password=,abcd,",
    ]
    results = utils.parse.parse_urls(", ".join(urls))
    assert isinstance(results, list)
    assert len(results) == len(urls)

    # No match because the comma is gone in the results entry
    # schema1://hostname?password=,abcd
    assert urls[0] not in results
    assert urls[0][:-1] in results

    # However we wouldn't have lost the comma in the second one:
    # schema2://hostname?password=,abcd,
    assert urls[1] in results

    # Pass the list in (as a list); results are the same
    results = utils.parse.parse_urls(urls)
    assert isinstance(results, list)
    assert len(results) == len(urls)

    # Pass in garbage
    results = utils.parse.parse_urls(object)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_urls(42)
    assert isinstance(results, list)
    assert len(results) == 0

    results = utils.parse.parse_urls([None, object, 42])
    assert isinstance(results, list)
    assert len(results) == 0

    # Verify trailing-whitespace runs complete in reasonable time.
    t0 = time.monotonic()
    results = utils.parse.parse_urls("https://example.com" + " " * 50000)
    assert time.monotonic() - t0 < 5.0
    assert results == ["https://example.com"]


def test_dict_full_update():
    """utils: dict_full_update() testing"""
    dict_1 = {
        "a": 1,
        "b": 2,
        "c": 3,
        "d": {
            "z": 27,
            "y": 26,
            "x": 25,
        },
    }

    dict_2 = {
        "d": {
            "x": "updated",
            "w": 24,
        },
        "c": "updated",
        "e": 5,
    }
    utils.logic.dict_full_update(dict_1, dict_2)
    # Dictionary 2 is untouched
    assert len(dict_2) == 3
    assert dict_2["c"] == "updated"
    assert dict_2["d"]["w"] == 24
    assert dict_2["d"]["x"] == "updated"
    assert dict_2["e"] == 5

    # Dictionary 3 however has entries from Dict 2 applied
    # without disrupting entries that were not matched.
    assert len(dict_1) == 5
    assert dict_1["a"] == 1
    assert dict_1["b"] == 2
    assert dict_1["c"] == "updated"
    assert dict_1["d"]["w"] == 24
    assert dict_1["d"]["x"] == "updated"
    assert dict_1["d"]["y"] == 26
    assert dict_1["d"]["z"] == 27
    assert dict_1["e"] == 5


def test_parse_list():
    """utils: parse_list() testing"""

    # A simple single array entry (As str)
    results = utils.parse.parse_list(
        ".mkv,.avi,.divx,.xvid,.mov,.wmv,.mp4,.mpg,.mpeg,.vob,.iso"
    )

    assert results == sorted(
        [
            ".divx",
            ".iso",
            ".mkv",
            ".mov",
            ".mpg",
            ".avi",
            ".mpeg",
            ".vob",
            ".xvid",
            ".wmv",
            ".mp4",
        ]
    )

    class StrangeObject:
        def __str__(self):
            return ".avi"

    # Now 2 lists with lots of duplicates and other delimiters
    results = utils.parse.parse_list(
        ".mkv,.avi,.divx,.xvid,.mov,.wmv,.mp4,.mpg .mpeg,.vob,,; ;",
        (".mkv,.avi,.divx,.xvid,.mov    ", "    .wmv,.mp4;.mpg,.mpeg,"),
        ".vob,.iso",
        [
            ".vob",
            [
                ".vob",
                ".mkv",
                StrangeObject(),
            ],
        ],
        StrangeObject(),
    )

    assert results == sorted(
        [
            ".divx",
            ".iso",
            ".mkv",
            ".mov",
            ".mpg",
            ".avi",
            ".mpeg",
            ".vob",
            ".xvid",
            ".wmv",
            ".mp4",
        ]
    )

    # Garbage in is removed
    assert utils.parse.parse_list(object(), 42, None) == []

    # Now a list with extras we want to add as strings
    # empty entries are removed
    results = utils.parse.parse_list(
        [
            ".divx",
            ".iso",
            ".mkv",
            ".mov",
            "",
            "  ",
            ".avi",
            ".mpeg",
            ".vob",
            ".xvid",
            ".mp4",
        ],
        ".mov,.wmv,.mp4,.mpg",
    )

    assert results == sorted(
        [
            ".divx",
            ".wmv",
            ".iso",
            ".mkv",
            ".mov",
            ".mpg",
            ".avi",
            ".vob",
            ".xvid",
            ".mpeg",
            ".mp4",
        ]
    )


def test_import_module(tmpdir):
    """utils: import_module testing"""
    # Prepare ourselves a file to work with
    bad_file_base = tmpdir.mkdir("a")
    bad_file = bad_file_base.join("README.md")
    bad_file.write(
        cleandoc("""
    I'm a README file, not a Python one.

    I can't be loaded
    """)
    )
    assert utils.module.import_module(str(bad_file), "invalidfile1") is None
    assert (
        utils.module.import_module(str(bad_file_base), "invalidfile2") is None
    )


def test_module_detection(tmpdir):
    """utils: test_module_detection() testing"""

    # Clear our working variables so they don't obstruct with the tests here
    N_MGR._paths_previously_scanned.clear()
    N_MGR._custom_module_map.clear()

    # Test case where we load invalid data
    N_MGR.module_detection(None)

    # Invalid data does not load anything
    assert len(N_MGR._paths_previously_scanned) == 0
    assert len(N_MGR._custom_module_map) == 0

    # Prepare ourselves a file to work with
    notify_hook_a_base = tmpdir.mkdir("a")
    notify_hook_a = notify_hook_a_base.join("hook.py")
    notify_hook_a.write(
        cleandoc("""
    from apprise.decorators import notify

    @notify(on="clihook")
    def mywrapper(body, title, notify_type, *args, **kwargs):
        pass
    """)
    )

    notify_ignore = notify_hook_a_base.join("README.md")
    notify_ignore.write(
        cleandoc("""
    We're not a .py file, so this file gets gracefully skipped
    """)
    )

    # Not previously loaded
    assert "clihook" not in N_MGR

    # load entry by string
    N_MGR.module_detection(str(notify_hook_a))
    N_MGR.module_detection(str(notify_ignore))
    N_MGR.module_detection(str(notify_hook_a_base))

    assert len(N_MGR._paths_previously_scanned) == 3
    assert len(N_MGR._custom_module_map) == 1

    # Now loaded
    assert "clihook" in N_MGR

    # load entry by array
    N_MGR.module_detection([str(notify_hook_a)])

    # No changes to our path
    assert len(N_MGR._paths_previously_scanned) == 3
    assert len(N_MGR._custom_module_map) == 1

    # Reset our variables for the next test
    N_MGR._paths_previously_scanned.clear()
    N_MGR._custom_module_map.clear()

    # Hidden files are ignored
    notify_hook_b_base = tmpdir.mkdir("b")
    notify_hook_b = notify_hook_b_base.join(".hook.py")
    notify_hook_b.write(
        cleandoc("""
    from apprise.decorators import notify

    # this is in a hidden file so it will not load
    @notify(on="hidden")
    def mywrapper(body, title, notify_type, *args, **kwargs):
        pass
    """)
    )

    assert "hidden" not in N_MGR

    N_MGR.module_detection([str(notify_hook_b)])

    # Verify that it did not load
    assert "hidden" not in N_MGR

    # Path was scanned; nothing loaded
    assert len(N_MGR._paths_previously_scanned) == 1
    assert len(N_MGR._custom_module_map) == 0

    # Reset our variables for the next test
    N_MGR._paths_previously_scanned.clear()
    N_MGR._custom_module_map.clear()

    # modules with no hooks found are ignored
    notify_hook_c_base = tmpdir.mkdir("c")
    notify_hook_c = notify_hook_c_base.join("empty.py")
    notify_hook_c.write("")

    N_MGR.module_detection([str(notify_hook_c)])

    # File was found, no custom modules
    assert len(N_MGR._paths_previously_scanned) == 1
    assert len(N_MGR._custom_module_map) == 0

    # A new path scanned
    N_MGR.module_detection([str(notify_hook_c_base)])
    assert len(N_MGR._paths_previously_scanned) == 2
    assert len(N_MGR._custom_module_map) == 0

    def create_hook(tdir, cache=True, on="valid1"):
        """Just a temporary hook creation tool for writing a working notify
        hook."""
        tdir.write(
            cleandoc(f"""
        from apprise.decorators import notify

        # this is a good hook but burried in hidden directory which won't
        # be accessed unless the file is pointed to via absolute path
        @notify(on="{on}")
        def mywrapper(body, title, notify_type, *args, **kwargs):
            pass
        """)
        )

        N_MGR.module_detection([str(tdir)], cache=cache)

    create_hook(notify_hook_c, on="valid1")
    assert "valid1" not in N_MGR

    # Even if we correct our empty file; the fact the directory has been
    # scanned and failed to load (same with file), it won't be loaded
    # a second time. This is intentional since module_detection() get's
    # called for every AppriseAsset() object creation.  This prevents us
    # from reloading conent over and over again wasting resources
    assert "valid1" not in N_MGR
    N_MGR.module_detection([str(notify_hook_c)])
    assert len(N_MGR._paths_previously_scanned) == 2
    assert len(N_MGR._custom_module_map) == 0

    # Even by absolute path...
    N_MGR.module_detection([str(notify_hook_c)])
    assert "valid1" not in N_MGR
    assert len(N_MGR._paths_previously_scanned) == 2
    assert len(N_MGR._custom_module_map) == 0

    # However we can bypass the cache if we really want to
    N_MGR.module_detection([str(notify_hook_c_base)], cache=False)
    assert "valid1" in N_MGR
    assert len(N_MGR._paths_previously_scanned) == 2
    assert len(N_MGR._custom_module_map) == 1

    # Bypassing it twice causes the module to load twice (not very efficient)
    # However we can bypass the cache if we really want to
    N_MGR.module_detection([str(notify_hook_c_base)], cache=False)
    assert "valid1" in N_MGR
    assert len(N_MGR._paths_previously_scanned) == 2
    assert len(N_MGR._custom_module_map) == 1

    # If you update the module (corrupting it in the process and reload)
    notify_hook_c.write(
        cleandoc("""
    raise ValueError
    """)
    )

    # Force no cache to cause the file to be replaced
    N_MGR.module_detection([str(notify_hook_c_base)], cache=False)

    # Our valid entry is no longer loaded
    assert "valid1" not in N_MGR

    # No change to scanned paths
    assert len(N_MGR._paths_previously_scanned) == 2
    # The previously loaded module is now gone
    assert len(N_MGR._custom_module_map) == 0

    # Reload our valid1 entry
    create_hook(notify_hook_c, on="valid1", cache=False)
    assert "valid1" in N_MGR
    assert len(N_MGR._paths_previously_scanned) == 2
    assert len(N_MGR._custom_module_map) == 1

    # Prepare an empty file
    notify_hook_c.write("")
    N_MGR.module_detection([str(notify_hook_c_base)], cache=False)

    # Our valid entry is no longer loaded
    assert "valid1" not in N_MGR
    assert len(N_MGR._paths_previously_scanned) == 2
    assert len(N_MGR._custom_module_map) == 0

    # Now reload our module again (this time rather then an exception, the
    # module is read back and swaps `valid1` for `valid2`
    create_hook(notify_hook_c, on="valid1", cache=False)
    assert "valid1" in N_MGR
    assert "valid2" not in N_MGR
    assert len(N_MGR._paths_previously_scanned) == 2
    assert len(N_MGR._custom_module_map) == 1

    create_hook(notify_hook_c, on="valid2", cache=False)
    assert "valid1" not in N_MGR
    assert "valid2" in N_MGR
    assert len(N_MGR._paths_previously_scanned) == 2
    assert len(N_MGR._custom_module_map) == 1

    # Reset our variables for the next test
    create_hook(notify_hook_c, on="valid1", cache=False)
    del N_MGR["valid1"]
    N_MGR._paths_previously_scanned.clear()
    N_MGR._custom_module_map.clear()

    notify_hook_d = notify_hook_c_base.join(".ignore.py")
    notify_hook_d.write("")
    notify_hook_e_base = notify_hook_c_base.mkdir(".ignore")
    notify_hook_e = notify_hook_e_base.join("__init__.py")
    notify_hook_e.write(
        cleandoc("""
    from apprise.decorators import notify

    # this is a good hook but burried in hidden directory which won't
    # be accessed unless the file is pointed to via absolute path
    @notify(on="valid2")
    def mywrapper(body, title, notify_type, *args, **kwargs):
        pass
    """)
    )

    # Try to load our base directory again; this time we search by the
    # directory; the only edge case we're testing here is it will not
    # even look at the .ignore.py file found since it is invalid
    N_MGR.module_detection([str(notify_hook_c_base)])
    assert "valid1" in N_MGR
    assert len(N_MGR._paths_previously_scanned) == 2
    assert len(N_MGR._custom_module_map) == 1

    # Reset our variables for the next test
    del N_MGR._schema_map["valid1"]
    N_MGR._paths_previously_scanned.clear()
    N_MGR._custom_module_map.clear()

    # Try to load our base directory again
    N_MGR.module_detection([str(notify_hook_c)])
    assert "valid1" in N_MGR

    # Hidden directories are not scanned
    assert "valid2" not in N_MGR

    assert len(N_MGR._paths_previously_scanned) == 1
    assert str(notify_hook_c) in N_MGR._paths_previously_scanned
    assert len(N_MGR._custom_module_map) == 1

    # However a direct reference to the hidden directory is okay
    N_MGR.module_detection([str(notify_hook_e_base)])

    # We loaded our module
    assert "valid2" in N_MGR
    assert len(N_MGR._paths_previously_scanned) == 3
    assert str(notify_hook_c) in N_MGR._paths_previously_scanned
    assert str(notify_hook_e) in N_MGR._paths_previously_scanned
    assert str(notify_hook_e_base) in N_MGR._paths_previously_scanned
    assert len(N_MGR._custom_module_map) == 2

    # Reset our variables for the next test
    del N_MGR._schema_map["valid1"]
    del N_MGR._schema_map["valid2"]
    N_MGR._paths_previously_scanned.clear()
    N_MGR._custom_module_map.clear()

    # Load our file directly
    assert "valid2" not in N_MGR
    N_MGR.module_detection([str(notify_hook_e)])

    # Now we have it loaded as expected
    assert "valid2" in N_MGR
    assert len(N_MGR._paths_previously_scanned) == 1
    assert str(notify_hook_e) in N_MGR._paths_previously_scanned
    assert len(N_MGR._custom_module_map) == 1

    # however if we try to load the base directory where the __init__.py
    # was already loaded from, it will not change anything
    N_MGR.module_detection([str(notify_hook_e_base)])
    assert "valid2" in N_MGR
    assert len(N_MGR._paths_previously_scanned) == 2
    assert str(notify_hook_e) in N_MGR._paths_previously_scanned
    assert str(notify_hook_e_base) in N_MGR._paths_previously_scanned
    assert len(N_MGR._custom_module_map) == 1

    # Tidy up for the next test
    del N_MGR._schema_map["valid2"]
    N_MGR._paths_previously_scanned.clear()
    N_MGR._custom_module_map.clear()

    assert "valid1" not in N_MGR
    assert "valid2" not in N_MGR
    assert "valid3" not in N_MGR
    notify_hook_f_base = tmpdir.mkdir("f")
    notify_hook_f = notify_hook_f_base.join("invalid.py")
    notify_hook_f.write(
        cleandoc("""
    from apprise.decorators import notify

    # A very invalid hook type... on should not be None
    @notify(on=None)
    def mywrapper(body, title, notify_type, *args, **kwargs):
        pass

    # An invalid name
    @notify(on='valid1', name=None)
    def mywrapper(body, title, notify_type, *args, **kwargs):
        pass

    # Another invalid name (so it's ignored)
    @notify(on='valid2', name=object)
    def mywrapper(body, title, notify_type, *args, **kwargs):
        pass

    # Simply put... the name has to be a string to be referenced
    # however this will still be loaded
    @notify(on='valid3', name=4)
    def mywrapper(body, title, notify_type, *args, **kwargs):
        pass

    """)
    )

    N_MGR.module_detection([str(notify_hook_f)])

    assert len(N_MGR._paths_previously_scanned) == 1
    assert len(N_MGR._custom_module_map) == 1
    assert "valid1" in N_MGR
    assert "valid2" in N_MGR
    assert "valid3" in N_MGR

    # Reset our variables for the next test
    del N_MGR._schema_map["valid1"]
    del N_MGR._schema_map["valid2"]
    del N_MGR._schema_map["valid3"]
    N_MGR._paths_previously_scanned.clear()
    N_MGR._custom_module_map.clear()

    # Now test the handling of just bad data entirely
    notify_hook_g_base = tmpdir.mkdir("g")
    notify_hook_g = notify_hook_g_base.join("binary.py")
    with open(str(notify_hook_g), "wb") as fout:
        fout.write(os.urandom(512))

    N_MGR.module_detection([str(notify_hook_g)])
    assert len(N_MGR._paths_previously_scanned) == 1
    assert len(N_MGR._custom_module_map) == 0

    # Reset our variables before we leave
    N_MGR._paths_previously_scanned.clear()
    N_MGR._custom_module_map.clear()


def test_exclusive_match():
    """utils: is_exclusive_match() testing"""

    # No Logic always returns True if there is also no data
    assert utils.logic.is_exclusive_match(data=None, logic=None) is True
    assert utils.logic.is_exclusive_match(data=None, logic=set()) is True
    assert utils.logic.is_exclusive_match(data="", logic=set()) is True
    assert utils.logic.is_exclusive_match(data="", logic=set()) is True

    # however, once data is introduced, True is no longer returned
    # if no logic has been specified
    assert utils.logic.is_exclusive_match(data="check", logic=set()) is False
    assert (
        utils.logic.is_exclusive_match(data=["check", "checkb"], logic=set())
        is False
    )

    # String delimters are stripped out so that a list can be formed
    # the below is just an empty token list
    assert utils.logic.is_exclusive_match(data=set(), logic=",;   ,") is True

    # garbage logic is never an exclusive match
    assert utils.logic.is_exclusive_match(data=set(), logic=object()) is False
    assert (
        utils.logic.is_exclusive_match(
            data=set(),
            logic=[
                object(),
            ],
        )
        is False
    )

    #
    # Test with logic:
    #
    data = {"abc"}

    # def in data
    assert utils.logic.is_exclusive_match(logic="def", data=data) is False
    # def in data
    assert (
        utils.logic.is_exclusive_match(
            logic=[
                "def",
            ],
            data=data,
        )
        is False
    )
    # def in data
    assert utils.logic.is_exclusive_match(logic=("def",), data=data) is False
    # def in data
    assert (
        utils.logic.is_exclusive_match(
            logic={
                "def",
            },
            data=data,
        )
        is False
    )
    # abc in data
    assert (
        utils.logic.is_exclusive_match(
            logic=[
                "abc",
            ],
            data=data,
        )
        is True
    )
    # abc in data
    assert utils.logic.is_exclusive_match(logic=("abc",), data=data) is True
    # abc in data
    assert (
        utils.logic.is_exclusive_match(
            logic={
                "abc",
            },
            data=data,
        )
        is True
    )
    # abc or def in data
    assert utils.logic.is_exclusive_match(logic="abc, def", data=data) is True

    #
    # Update our data set so we can do more advance checks
    #
    data = {"abc", "def", "efg", "xyz"}

    # match_all matches everything
    assert utils.logic.is_exclusive_match(logic="all", data=data) is True
    assert utils.logic.is_exclusive_match(logic=["all"], data=data) is True

    # def and abc in data
    assert (
        utils.logic.is_exclusive_match(logic=[("abc", "def")], data=data)
        is True
    )

    # cba and abc in data
    assert (
        utils.logic.is_exclusive_match(logic=[("cba", "abc")], data=data)
        is False
    )

    # www or zzz or abc and xyz
    assert (
        utils.logic.is_exclusive_match(
            logic=["www", "zzz", ("abc", "xyz")], data=data
        )
        is True
    )
    # www or zzz or abc and xyz (strings are valid too)
    assert (
        utils.logic.is_exclusive_match(
            logic=["www", "zzz", "abc, xyz"], data=data
        )
        is True
    )

    # www or zzz or abc and jjj
    assert (
        utils.logic.is_exclusive_match(
            logic=["www", "zzz", ("abc", "jjj")], data=data
        )
        is False
    )

    #
    # Empty data set
    #
    data = set()
    assert utils.logic.is_exclusive_match(logic=["www"], data=data) is False
    assert utils.logic.is_exclusive_match(logic="all", data=data) is True

    #
    # Update our data set so we can do more advance checks
    #
    data = {"always", "entry1"}
    # We'll always match on the with keyword always
    assert utils.logic.is_exclusive_match(logic="always", data=data) is True
    assert utils.logic.is_exclusive_match(logic="garbage", data=data) is True
    # However we will not match if we turn this feature off
    assert (
        utils.logic.is_exclusive_match(
            logic="garbage", data=data, match_always=False
        )
        is False
    )

    # Change default value from 'all' to 'match_me'. Logic matches
    # so we pass
    assert (
        utils.logic.is_exclusive_match(
            logic="match_me", data=data, match_all="match_me"
        )
        is True
    )


def test_exclusive_match_priority_tokens():
    """utils: is_exclusive_match() with priority-prefixed filter tokens"""

    # AppriseTag.__hash__ is name-only, so a set holds at most one entry per
    # tag name -- matching real usage where each URL has one priority.
    tag_ep3 = AppriseTag("endpoint", priority=3, has_priority=True)
    tag_other = AppriseTag("other", priority=1, has_priority=True)

    data = {tag_ep3, tag_other}

    # Exact priority match
    assert (
        utils.logic.is_exclusive_match(logic="3:endpoint", data=data) is True
    )

    # Priority mismatch -- "2:endpoint" does not match priority-3 entry
    assert (
        utils.logic.is_exclusive_match(logic="2:endpoint", data=data) is False
    )

    # Name-only filter matches via O(1) hash path regardless of stored priority
    assert utils.logic.is_exclusive_match(logic="endpoint", data=data) is True

    # Absent tag returns False
    assert utils.logic.is_exclusive_match(logic="absent", data=data) is False

    # AND group: both tokens present with correct priorities
    assert (
        utils.logic.is_exclusive_match(
            logic=[("3:endpoint", "1:other")], data=data
        )
        is True
    )

    # AND group: one priority mismatch fails the whole group
    assert (
        utils.logic.is_exclusive_match(
            logic=[("2:endpoint", "1:other")], data=data
        )
        is False
    )

    # OR: failing AND group rescued by a matching single token
    assert (
        utils.logic.is_exclusive_match(
            logic=[("2:endpoint", "1:other"), "3:endpoint"], data=data
        )
        is True
    )

    # OR: no term matches -- all fail
    assert (
        utils.logic.is_exclusive_match(
            logic=[("2:endpoint", "1:other"), "99:endpoint"], data=data
        )
        is False
    )

    # Backward compat: plain strings in data matched by a priority-prefixed
    # filter token (the for-loop fallback in _token_matches_data)
    str_data = {"endpoint", "other"}
    assert (
        utils.logic.is_exclusive_match(logic="7:endpoint", data=str_data)
        is True
    )
    assert (
        utils.logic.is_exclusive_match(logic="7:absent", data=str_data)
        is False
    )

    # match_all keyword short-circuits even with priority data
    assert utils.logic.is_exclusive_match(logic="all", data=data) is True


def test_apprise_validate_regex():
    """
    API: Apprise() Validate Regex tests

    """
    assert utils.parse.validate_regex(None) is None
    assert utils.parse.validate_regex(object) is None
    assert utils.parse.validate_regex(42) is None
    assert utils.parse.validate_regex("") is None
    assert utils.parse.validate_regex("  ") is None
    assert utils.parse.validate_regex("abc") == "abc"

    # value is a keyword that is extracted (if found)
    assert (
        utils.parse.validate_regex(
            "- abcd -", r"-(?P<value>[^-]+)-", fmt="{value}"
        )
        == "abcd"
    )
    assert (
        utils.parse.validate_regex(
            "- abcd -", r"-(?P<value>[^-]+)-", strip=False, fmt="{value}"
        )
        == " abcd "
    )

    # String flags supported in addition to numeric
    assert (
        utils.parse.validate_regex(
            "- abcd -", r"-(?P<value>[^-]+)-", "i", fmt="{value}"
        )
        == "abcd"
    )
    assert (
        utils.parse.validate_regex(
            "- abcd -", r"-(?P<value>[^-]+)-", re.I, fmt="{value}"
        )
        == "abcd"
    )

    # Test multiple flag settings
    assert (
        utils.parse.validate_regex(
            "- abcd -", r"-(?P<value>[^-]+)-", "isax", fmt="{value}"
        )
        == "abcd"
    )

    # Invalid flags are just ignored. The below fails to match
    # because the default value of 'i' is over-ridden by what is
    # identfied below, and no flag is set at the end of the day
    assert (
        utils.parse.validate_regex(
            "- abcd -", r"-(?P<value>[ABCD]+)-", "-%2gb", fmt="{value}"
        )
        is None
    )
    assert (
        utils.parse.validate_regex(
            "- abcd -", r"-(?P<value>[ABCD]+)-", "", fmt="{value}"
        )
        is None
    )
    assert (
        utils.parse.validate_regex(
            "- abcd -", r"-(?P<value>[ABCD]+)-", None, fmt="{value}"
        )
        is None
    )


def test_apply_templating():
    """utils: apply_template() testing"""

    template = "Hello {{fname}}, How are you {{whence}}?"

    result = utils.templates.apply_template(
        template, **{"fname": "Chris", "whence": "this morning"}
    )
    assert isinstance(result, str)
    assert result == "Hello Chris, How are you this morning?"

    # In this example 'whence' isn't provided, so it isn't swapped
    result = utils.templates.apply_template(template, **{"fname": "Chris"})
    assert isinstance(result, str)
    assert result == "Hello Chris, How are you {{whence}}?"

    # white space won't cause any ill affects:
    template = "Hello {{ fname }}, How are you {{   whence}}?"
    result = utils.templates.apply_template(
        template, **{"fname": "Chris", "whence": "this morning"}
    )
    assert isinstance(result, str)
    assert result == "Hello Chris, How are you this morning?"

    # No arguments won't cause any problems
    template = "Hello {{fname}}, How are you {{whence}}?"
    result = utils.templates.apply_template(template)
    assert isinstance(result, str)
    assert result == template

    # Wrong elements are simply ignored
    result = utils.templates.apply_template(
        template, **{"fname": "l2g", "whence": "this evening", "ignore": "me"}
    )
    assert isinstance(result, str)
    assert result == "Hello l2g, How are you this evening?"

    # Empty template makes things easy
    result = utils.templates.apply_template(
        "", **{"fname": "l2g", "whence": "this evening"}
    )
    assert isinstance(result, str)
    assert result == ""

    # Regular expressions are safely escapped and act as normal
    # tokens:
    template = "Hello {{.*}}, How are you {{[A-Z0-9]+}}?"
    result = utils.templates.apply_template(
        template, **{".*": "l2g", "[A-Z0-9]+": "this afternoon"}
    )
    assert result == "Hello l2g, How are you this afternoon?"

    # JSON is handled too such as escaping quotes
    template = '{value: "{{ value }}"}'
    result = utils.templates.apply_template(
        template,
        app_mode=utils.templates.TemplateType.JSON,
        **{"value": '"quotes are escaped"'},
    )
    assert result == '{value: "\\"quotes are escaped\\""}'


def test_cwe312_word():
    """utils: cwe312_word() testing"""
    assert utils.cwe312.cwe312_word(None) is None
    assert utils.cwe312.cwe312_word(42) == 42
    assert utils.cwe312.cwe312_word("") == ""
    assert utils.cwe312.cwe312_word(" ") == " "
    assert utils.cwe312.cwe312_word("!") == "!"

    assert utils.cwe312.cwe312_word("a") == "a"
    assert utils.cwe312.cwe312_word("ab") == "ab"
    assert utils.cwe312.cwe312_word("abc") == "abc"
    assert utils.cwe312.cwe312_word("abcd") == "abcd"
    assert utils.cwe312.cwe312_word("abcd", force=True) == "a...d"

    assert utils.cwe312.cwe312_word("abc--d") == "abc--d"
    assert utils.cwe312.cwe312_word("a-domain.ca") == "a...a"

    # Variances to still catch domain
    assert (
        utils.cwe312.cwe312_word("a-domain.ca", advanced=False)
        == "a-domain.ca"
    )
    assert (
        utils.cwe312.cwe312_word("a-domain.ca", threshold=6) == "a-domain.ca"
    )


def test_cwe312_url():
    """utils: cwe312_url() testing"""
    assert utils.cwe312.cwe312_url(None) is None
    assert utils.cwe312.cwe312_url(42) == 42
    assert utils.cwe312.cwe312_url("http://") == "http://"
    assert utils.cwe312.cwe312_url("discord://") == "discord://"
    assert utils.cwe312.cwe312_url("path") == "http://path"
    assert utils.cwe312.cwe312_url("path/") == "http://path/"

    # Now test http:// private data
    assert (
        utils.cwe312.cwe312_url("http://user:pass123@localhost")
        == "http://user:p...3@localhost"
    )
    assert (
        utils.cwe312.cwe312_url("http://user@localhost")
        == "http://user@localhost"
    )
    assert (
        utils.cwe312.cwe312_url("http://user@localhost?password=abc123")
        == "http://user@localhost?password=a...3"
    )
    assert (
        utils.cwe312.cwe312_url("http://user@localhost?secret=secret-.12345")
        == "http://user@localhost?secret=s...5"
    )

    assert (
        utils.cwe312.cwe312_url(
            "slack://mybot@xoxb-43598234231-3248932482278"
            "-BZK5Wj15B9mPh1RkShJoCZ44"
            "/lead2gold@gmail.com"
        )
        == "slack://mybot@x...4/l...m"
    )
    assert (
        utils.cwe312.cwe312_url(
            "slack://test@B4QP3WWB4/J3QWT41JM/XIl2ffpqXkzkwMXrJdevi7W3/#random"
        )
        == "slack://test@B...4/J...M/X...3/"
    )


def test_base64_encode_decode():
    """Utils:Base64:URLEncode & Decode."""
    assert utils.base64.base64_urlencode(None) is None
    assert utils.base64.base64_urlencode(42) is None
    assert utils.base64.base64_urlencode(object) is None
    assert utils.base64.base64_urlencode({}) is None
    assert utils.base64.base64_urlencode("") is None
    assert utils.base64.base64_urlencode("abc") is None
    assert utils.base64.base64_urlencode(b"") == ""
    assert utils.base64.base64_urlencode(b"abc") == "YWJj"

    assert utils.base64.base64_urldecode(None) is None
    assert utils.base64.base64_urldecode(42) is None
    assert utils.base64.base64_urldecode(object) is None
    assert utils.base64.base64_urldecode({}) is None

    assert utils.base64.base64_urldecode("abc") == b"i\xb7"
    assert utils.base64.base64_urldecode("") == b""
    assert utils.base64.base64_urldecode("YWJj") == b"abc"


def test_dict_base64_codec(tmpdir):
    """Test encoding/decoding of base64 content."""
    original = {
        "int": 1,
        "float": 2.3,
    }

    encoded, needs_decoding = utils.base64.encode_b64_dict(original)
    assert encoded == {"int": "b64:MQ==", "float": "b64:Mi4z"}
    assert needs_decoding is True
    decoded = utils.base64.decode_b64_dict(encoded)
    assert decoded == original

    with mock.patch("json.dumps", side_effect=TypeError()):
        encoded, needs_decoding = utils.base64.encode_b64_dict(original)
        # we failed
        assert needs_decoding is False
        assert encoded == {
            "int": "1",
            "float": "2.3",
        }


def test_dir_size(tmpdir):
    """Test dir size tool."""

    # Nothing to find/see
    size, errors = utils.disk.dir_size(str(tmpdir))
    assert size == 0
    assert len(errors) == 0

    # Write a file in our root directory
    tmpdir.join("root.psdata").write("0" * 1024 * 1024)

    # Prepare some more directories
    namespace_1 = tmpdir.mkdir("abcdefg")
    namespace_2 = tmpdir.mkdir("defghij")
    namespace_2.join("cache.psdata").write("0" * 1024 * 1024)
    size, errors = utils.disk.dir_size(str(tmpdir))
    assert size == 1024 * 1024 * 2
    assert len(errors) == 0

    # Write another file
    namespace_1.join("cache.psdata").write("0" * 1024 * 1024)
    size, errors = utils.disk.dir_size(str(tmpdir))
    assert size == 1024 * 1024 * 3
    assert len(errors) == 0

    size, errors = utils.disk.dir_size(str(namespace_1))
    assert size == 1024 * 1024
    assert len(errors) == 0

    # Create a directory insde one of our namespaces
    subspace_1 = namespace_1.mkdir("zyx")
    size, errors = utils.disk.dir_size(str(namespace_1))
    assert size == 1024 * 1024

    subspace_1.join("cache.psdata").write("0" * 1024 * 1024)
    size, errors = utils.disk.dir_size(str(tmpdir))
    assert size == 1024 * 1024 * 4
    assert len(errors) == 0

    # Recursion limit reduced... no change at 2 as we can go 2
    # diretories deep no problem
    size, errors = utils.disk.dir_size(str(tmpdir), max_depth=2)
    assert size == 1024 * 1024 * 4
    assert len(errors) == 0

    size, errors = utils.disk.dir_size(str(tmpdir), max_depth=1)
    assert size == 1024 * 1024 * 3
    # we can't get into our subspace_1
    assert len(errors) == 1
    assert str(subspace_1) in errors

    size, errors = utils.disk.dir_size(str(tmpdir), max_depth=0)
    assert size == 1024 * 1024
    # we can't get into our namespace directories
    assert len(errors) == 2
    assert str(namespace_1) in errors
    assert str(namespace_2) in errors

    # Let's cause problems now and test the output
    size, errors = utils.disk.dir_size("invalid-directory", missing_okay=True)
    assert size == 0
    assert len(errors) == 0

    size, errors = utils.disk.dir_size("invalid-directory", missing_okay=False)
    assert size == 0
    assert len(errors) == 1
    assert "invalid-directory" in errors

    with mock.patch("os.scandir", side_effect=OSError()):
        size, errors = utils.disk.dir_size(str(tmpdir), missing_okay=True)
        assert size == 0
        assert len(errors) == 1
        assert str(tmpdir) in errors

    with mock.patch("os.scandir") as mock_scandir:
        mock_entry = mock.MagicMock()
        mock_entry.is_file.side_effect = OSError()
        mock_entry.path = "/test/path"
        # Mock the scandir return value to yield the mock entry
        mock_scandir.return_value.__enter__.return_value = [mock_entry]

        size, errors = utils.disk.dir_size(str(tmpdir))
        assert size == 0
        assert len(errors) == 1
        assert mock_entry.path in errors

    with mock.patch("os.scandir") as mock_scandir:
        mock_entry = mock.MagicMock()
        mock_entry.is_file.return_value = False
        mock_entry.is_dir.side_effect = OSError()
        mock_entry.path = "/test/path"
        # Mock the scandir return value to yield the mock entry
        mock_scandir.return_value.__enter__.return_value = [mock_entry]
        size, errors = utils.disk.dir_size(str(tmpdir))
        assert len(errors) == 1
        assert mock_entry.path in errors

    with mock.patch("os.scandir") as mock_scandir:
        mock_entry = mock.MagicMock()
        mock_entry.is_file.return_value = False
        mock_entry.is_dir.return_value = False
        # Mock the scandir return value to yield the mock entry
        mock_scandir.return_value.__enter__.return_value = [mock_entry]
        size, errors = utils.disk.dir_size(str(tmpdir))
        assert size == 0
        assert len(errors) == 0

    with mock.patch("os.scandir") as mock_scandir:
        mock_entry = mock.MagicMock()
        mock_entry.is_file.side_effect = FileNotFoundError()
        mock_entry.path = "/test/path"
        # Mock the scandir return value to yield the mock entry
        mock_scandir.return_value.__enter__.return_value = [mock_entry]

        size, errors = utils.disk.dir_size(str(tmpdir))
        assert size == 0
        # No file isn't a problem, we're calculating disksize anyway,
        # one less thing to calculate
        assert len(errors) == 0


def test_bytes_to_str():
    """Test Bytes to String representation."""
    # Garbage Entry
    assert utils.disk.bytes_to_str(None) is None
    assert utils.disk.bytes_to_str("") is None
    assert utils.disk.bytes_to_str("GARBAGE") is None

    # Good Entries
    assert utils.disk.bytes_to_str(0) == "0.00B"
    assert utils.disk.bytes_to_str(1) == "1.00B"
    assert utils.disk.bytes_to_str(1.1) == "1.10B"
    assert utils.disk.bytes_to_str(1024) == "1.00KB"
    assert utils.disk.bytes_to_str(1024 * 1024) == "1.00MB"
    assert utils.disk.bytes_to_str(1024 * 1024 * 1024) == "1.00GB"
    assert utils.disk.bytes_to_str(1024 * 1024 * 1024 * 1024) == "1.00TB"

    # Support strings too
    assert utils.disk.bytes_to_str("0") == "0.00B"
    assert utils.disk.bytes_to_str("1024") == "1.00KB"


def test_time_zoneinfo():
    """utils: zoneinfo() testing"""

    # Some valid strings
    assert isinstance(utils.time.zoneinfo("UTC"), tzinfo)
    assert isinstance(utils.time.zoneinfo("z"), tzinfo)
    assert isinstance(utils.time.zoneinfo("gmt"), tzinfo)
    assert isinstance(utils.time.zoneinfo("utc"), tzinfo)
    assert isinstance(utils.time.zoneinfo("Toronto"), tzinfo)
    assert isinstance(utils.time.zoneinfo("America/Toronto"), tzinfo)
    assert isinstance(utils.time.zoneinfo("america/toronto"), tzinfo)

    # Edge Case with time also supported:
    tz = utils.time.zoneinfo("America/Argentina/Cordoba")
    isinstance(tz, tzinfo)
    assert isinstance(utils.time.zoneinfo("Argentina/Cordoba"), tzinfo)
    assert utils.time.zoneinfo("Argentina/Cordoba").key == tz.key
    # "America/Cordoba" has been obsoleted by IANA in facor of
    #  "America/Argentina/Cordoba", however the IANA database
    #  instance used is system-dependent, so these tests have
    #  different results depending on the system running them
    if utils.time.zoneinfo("Cordoba") is not None:
        # the system has the obsolete "America/Cordoba" entry
        assert isinstance(utils.time.zoneinfo("Cordoba"), tzinfo)
        assert utils.time.zoneinfo("Cordoba").key == "America/Cordoba"
    else:
        assert utils.time.zoneinfo("Cordoba") is None
        # the utils helper should still resolve this abbreviated (and
        #  lowercase) form
        assert (
            utils.time.zoneinfo("argentina/cordoba").key
            == "America/Argentina/Cordoba"
        )

    # Too ambiguous
    assert utils.time.zoneinfo("Argentina") is None

    # bad data
    assert utils.time.zoneinfo(object) is None
    assert utils.time.zoneinfo(None) is None
    assert utils.time.zoneinfo(1) is None
    assert utils.time.zoneinfo("") is None
    assert utils.time.zoneinfo("invalid") is None
