# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2025, 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.

import pytest
from apprise import AppriseAsset
from apprise.config import ConfigBase
from apprise import Apprise
from apprise import ConfigFormat
from inspect import cleandoc
import yaml

# Disable logging for a cleaner testing output
import logging

from apprise.plugins.email import NotifyEmail

logging.disable(logging.CRITICAL)


def test_config_base():
    """
    API: ConfigBase() object

    """

    # invalid types throw exceptions
    with pytest.raises(TypeError):
        ConfigBase(**{'format': 'invalid'})

    # Config format types are not the same as ConfigBase ones
    with pytest.raises(TypeError):
        ConfigBase(**{'format': 'markdown'})

    cb = ConfigBase(**{'format': 'yaml'})
    assert isinstance(cb, ConfigBase)

    cb = ConfigBase(**{'format': 'text'})
    assert isinstance(cb, ConfigBase)

    # Set encoding
    cb = ConfigBase(encoding='utf-8', format='text')
    assert isinstance(cb, ConfigBase)

    # read is not supported in the base object; only the children
    assert cb.read() is None

    # There are no servers loaded on a freshly created object
    assert len(cb.servers()) == 0

    # Unsupported URLs are not parsed
    assert ConfigBase.parse_url(url='invalid://') is None

    # Valid URL & Valid Format
    results = ConfigBase.parse_url(
        url='file://relative/path?format=yaml&encoding=latin-1')
    assert isinstance(results, dict)
    # These are moved into the root
    assert results.get('format') == 'yaml'
    assert results.get('encoding') == 'latin-1'

    # But they also exist in the qsd location
    assert isinstance(results.get('qsd'), dict)
    assert results['qsd'].get('encoding') == 'latin-1'
    assert results['qsd'].get('format') == 'yaml'

    # Valid URL & Invalid Format
    results = ConfigBase.parse_url(
        url='file://relative/path?format=invalid&encoding=latin-1')
    assert isinstance(results, dict)
    # Only encoding is moved into the root
    assert 'format' not in results
    assert results.get('encoding') == 'latin-1'

    # But they will always exist in the qsd location
    assert isinstance(results.get('qsd'), dict)
    assert results['qsd'].get('encoding') == 'latin-1'
    assert results['qsd'].get('format') == 'invalid'


def test_config_base_detect_config_format():
    """
    API: ConfigBase.detect_config_format

    """

    # Garbage Handling
    for garbage in (object(), None, 42):
        # A response is always correctly returned
        assert ConfigBase.detect_config_format(garbage) is None

    # Empty files are valid
    assert ConfigBase.detect_config_format('') is ConfigFormat.TEXT

    # Valid Text Configuration
    assert ConfigBase.detect_config_format("""
    # A comment line over top of a URL
    mailto://userb:pass@gmail.com
    """) is ConfigFormat.TEXT

    # A text file that has semi-colon as comment characters
    # is valid too
    assert ConfigBase.detect_config_format("""
    ; A comment line over top of a URL
    mailto://userb:pass@gmail.com
    """) is ConfigFormat.TEXT

    # Valid YAML Configuration
    assert ConfigBase.detect_config_format("""
    # A comment line over top of a URL
    version: 1
    """) is ConfigFormat.YAML

    # Just a whole lot of blank lines...
    assert ConfigBase.detect_config_format('\n\n\n') is ConfigFormat.TEXT

    # Invalid Config
    assert ConfigBase.detect_config_format("3") is None


def test_config_base_config_parse():
    """
    API: ConfigBase.config_parse

    """

    # Garbage Handling
    for garbage in (object(), None, 42):
        # A response is always correctly returned
        result = ConfigBase.config_parse(garbage)
        # response is a tuple...
        assert isinstance(result, tuple)
        # containing 2 items (plugins, config)
        assert len(result) == 2
        # In the case of garbage in, we get garbage out; both lists are empty
        assert result == (list(), list())

    # Valid Text Configuration
    result = ConfigBase.config_parse("""
    # A comment line over top of a URL
    mailto://userb:pass@gmail.com
    """, asset=AppriseAsset())
    # We expect to parse 1 entry from the above
    assert isinstance(result, tuple)
    assert len(result) == 2
    # The first element is the number of notification services processed
    assert len(result[0]) == 1
    # If we index into the item, we can check to see the tags associate
    # with it
    assert len(result[0][0].tags) == 0

    # The second is the number of configuration include lines parsed
    assert len(result[1]) == 0

    # Valid Configuration
    result = ConfigBase.config_parse("""
# if no version is specified then version 1 is presumed
version: 1

#
# Define your notification urls:
#
urls:
  - pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b
  - mailto://test:password@gmail.com
  - json://localhost:
      - tag: devops, admin
    """, asset=AppriseAsset())

    # We expect to parse 2 entries from the above
    assert isinstance(result, tuple)
    assert len(result) == 2
    assert isinstance(result[0], list)
    assert len(result[0]) == 3
    assert len(result[0][0].tags) == 0
    assert len(result[0][1].tags) == 0
    assert len(result[0][2].tags) == 2

    # Test case where we pass in a bad format
    result = ConfigBase.config_parse("""
    ; A comment line over top of a URL
    mailto://userb:pass@gmail.com
    """, config_format='invalid-format')

    # This is not parseable despite the valid text
    assert isinstance(result, tuple)
    assert isinstance(result[0], list)
    assert len(result[0]) == 0

    result, _ = ConfigBase.config_parse("""
    ; A comment line over top of a URL
    mailto://userb:pass@gmail.com
    """, config_format=ConfigFormat.TEXT)

    # Parseable
    assert isinstance(result, list)
    assert len(result) == 1


def test_config_base_discord_bug_report_01():
    """
    API: ConfigBase.config_parse user feedback

    A Discord report that a tag was not correctly assigned to a URL when
    presented in the following format
       urls:
         - json://myhost:
           - tag: test
             userid: test
    """
    result, config = ConfigBase.config_parse("""
    urls:
      - json://myhost:
        - tag: test
          userid: test
    """, asset=AppriseAsset())

    # We expect to parse 4 entries from the above
    assert isinstance(result, list)
    assert isinstance(config, list)
    assert len(result) == 1
    assert len(result[0].tags) == 1
    assert 'test' in result[0].tags


def test_config_base_config_parse_text():
    """
    API: ConfigBase.config_parse_text object

    """

    # Garbage Handling
    for garbage in (object(), None, 42):
        # A response is always correctly returned
        result = ConfigBase.config_parse_text(garbage)
        # response is a tuple...
        assert isinstance(result, tuple)
        # containing 2 items (plugins, config)
        assert len(result) == 2
        # In the case of garbage in, we get garbage out; both lists are empty
        assert result == (list(), list())

    # Valid Configuration
    result, config = ConfigBase.config_parse_text("""
    # A completely invalid token on json string (it gets ignored)
    # but the URL is still valid
    json://localhost?invalid-token=nodashes

    # A comment line over top of a URL
    mailto://userb:pass@gmail.com

    # Test a URL using it's native format; in this case Ryver
    https://apprise.ryver.com/application/webhook/ckhrjW8w672m6HG

    # Invalid URL as it's not associated with a plugin
    # or a native url
    https://not.a.native.url/

    # A line with mulitiple tag assignments to it
    taga,tagb=kde://

    # An include statement to Apprise API with trailing spaces:
    include http://localhost:8080/notify/apprise

    # A relative include statement (with trailing spaces)
    include apprise.cfg     """, asset=AppriseAsset())

    # We expect to parse 4 entries from the above
    assert isinstance(result, list)
    assert isinstance(config, list)
    assert len(result) == 4
    assert len(result[0].tags) == 0

    # Our last element will have 2 tags associated with it
    assert len(result[-1].tags) == 2
    assert 'taga' in result[-1].tags
    assert 'tagb' in result[-1].tags

    assert len(config) == 2
    assert 'http://localhost:8080/notify/apprise' in config
    assert 'apprise.cfg' in config

    # Here is a similar result set however this one has an invalid line
    # in it which invalidates the entire file
    result, config = ConfigBase.config_parse_text("""
    # A comment line over top of a URL
    mailto://userc:pass@gmail.com

    # A line with mulitiple tag assignments to it
    taga,tagb=windows://

    I am an invalid line that does not follow any of the Apprise file rules!
    """)

    # We expect to parse 0 entries from the above because the invalid line
    # invalidates the entire configuration file. This is for security reasons;
    # we don't want to point at files load content in them just because they
    # resemble an Apprise configuration.
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # More invalid data
    result, config = ConfigBase.config_parse_text("""
    # An invalid URL
    invalid://user:pass@gmail.com

    # A tag without a url
    taga=

    # A very poorly structured url
    sns://:@/

    # Just 1 token provided
    sns://T1JJ3T3L2/

    # Even with the above invalid entries, we can still
    # have valid include lines
    include file:///etc/apprise.cfg

    # An invalid include (nothing specified afterwards)
    include

    # An include of a config type we don't support
    include invalid://
    """)

    # We expect to parse 0 entries from the above
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # Test case where a comment is on it's own line with nothing else
    result, config = ConfigBase.config_parse_text("#")
    # We expect to parse 0 entries from the above
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # Verify our tagging works when multiple tags are provided
    result, config = ConfigBase.config_parse_text("""
    tag1, tag2, tag3=json://user:pass@localhost
    """)

    assert isinstance(result, list)
    assert len(result) == 1
    assert len(result[0].tags) == 3
    assert 'tag1' in result[0].tags
    assert 'tag2' in result[0].tags
    assert 'tag3' in result[0].tags


def test_config_base_config_tag_groups_text():
    """
    API: ConfigBase.config_tag_groups_text object

    """

    # Valid Configuration
    result, config = ConfigBase.config_parse_text("""
    # Tag assignments
    groupA, groupB = tagB, tagC

    # groupB doubles down as it takes the entries initialized above
    # plus the added ones defined below
    groupB = tagA, tagB, tagD
    groupC = groupA, groupB, groupC, tagE

    # Tag that recursively looks to more tags
    groupD = groupC

    # Assigned ourselves
    groupX = groupX

    # Set up a recursive loop
    groupE = groupF
    groupF = groupE

    # Set up a larger recursive loop
    groupG = groupH
    groupH = groupI
    groupI = groupJ
    groupJ = groupK
    groupK = groupG

    # Bad assignments
    groupM = , , ,
     , ,   = , , ,

    # int's and floats are okay
    1 = 2
    a = 5

    # A comment line over top of a URL
    4, groupB = mailto://userb:pass@gmail.com

    # Tag Assignments
    tagA,groupB=json://localhost

    # More Tag Assignments
    tagC,groupB=xml://localhost

    # More Tag Assignments
    groupD=form://localhost

    """, asset=AppriseAsset())

    # We expect to parse 4 entries from the above
    assert isinstance(result, list)
    assert isinstance(config, list)
    assert len(result) == 4

    # Our first element is our group tags
    assert len(result[0].tags) == 2
    assert 'groupB' in result[0].tags
    assert '4' in result[0].tags

    # No additional configuration is loaded
    assert len(config) == 0

    apobj = Apprise()
    assert apobj.add(result)
    # We match against 1 entry
    assert len([x for x in apobj.find('tagA')]) == 1
    assert len([x for x in apobj.find('tagB')]) == 0
    assert len([x for x in apobj.find('groupA')]) == 1
    assert len([x for x in apobj.find('groupB')]) == 3
    assert len([x for x in apobj.find('groupC')]) == 2
    assert len([x for x in apobj.find('groupD')]) == 3

    # Invalid Assignment
    result, config = ConfigBase.config_parse_text("""
    # Must have something to equal or it's a bad line
    group =

    # A tag Assignments that is never gotten to as the line
    # above is bad
    groupD=form://localhost
    """)

    # We expect to parse 0 entries from the above
    assert isinstance(result, list)
    assert isinstance(config, list)
    assert len(result) == 0
    assert len(config) == 0

    # Invalid Assignment
    result, config = ConfigBase.config_parse_text("""
    # Rundant assignment
    group = group

    # Our group assignment
    group=windows://

    """)

    # the redundant assignment does us no harm; but it doesn't grant us any
    # value either
    assert isinstance(result, list)
    assert len(result) == 1

    # Our first element is our group tags
    assert len(result[0].tags) == 1
    assert 'group' in result[0].tags

    # There were no include entries defined
    assert len(config) == 0

    # More invalid data
    result, config = ConfigBase.config_parse_text("""
    # A tag without a url or group assignment
    taga=

    """)

    # We expect to parse 0 entries from the above
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    result, config = ConfigBase.config_parse_text("""
    # A tag without a url or group assignment
    taga= %%INVALID
    """)

    # We expect to parse 0 entries from the above
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0


def test_config_base_config_parse_text_with_url():
    """
    API: ConfigBase.config_parse_text object_with_url

    """
    # Here is a similar result set however this one has an invalid line
    # in it which invalidates the entire file
    result, config = ConfigBase.config_parse_text("""
    # Test a URL that has a URL as an argument
    json://user:pass@localhost?+arg=http://example.com?arg2=1&arg3=3
    """)

    # No tag is parsed, but our URL successfully parses as is

    assert isinstance(result, list)
    assert len(result) == 1
    assert len(result[0].tags) == 0

    # Verify our URL is correctly captured
    assert '%2Barg=http%3A%2F%2Fexample.com%3Farg2%3D1' in result[0].url()
    assert 'json://user:pass@localhost/' in result[0].url()

    # There were no include entries defined
    assert len(config) == 0

    # Pass in our configuration again
    result, config = ConfigBase.config_parse_text(result[0].url())

    # Verify that our results repeat themselves
    assert isinstance(result, list)
    assert len(result) == 1
    assert len(result[0].tags) == 0
    assert '%2Barg=http%3A%2F%2Fexample.com%3Farg2%3D1' in result[0].url()
    assert 'json://user:pass@localhost/' in result[0].url()

    assert len(config) == 0


def test_config_base_config_parse_yaml():
    """
    API: ConfigBase.config_parse_yaml object

    """

    # general reference used below
    asset = AppriseAsset()

    # Garbage Handling
    for garbage in (object(), None, '', 42):
        # A response is always correctly returned
        result = ConfigBase.config_parse_yaml(garbage)
        # response is a tuple...
        assert isinstance(result, tuple)
        # containing 2 items (plugins, config)
        assert len(result) == 2
        # In the case of garbage in, we get garbage out; both lists are empty
        assert result == (list(), list())

    # Invalid Version
    result, config = ConfigBase.config_parse_yaml("version: 2a", asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # Invalid Syntax (throws a ScannerError)
    result, config = ConfigBase.config_parse_yaml("""
# if no version is specified then version 1 is presumed
version: 1

urls
""", asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # Missing url token
    result, config = ConfigBase.config_parse_yaml("""
# if no version is specified then version 1 is presumed
version: 1

""", asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # No urls defined
    result, config = ConfigBase.config_parse_yaml("""
# if no version is specified then version 1 is presumed
version: 1

urls:
""", asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # Invalid url defined
    result, config = ConfigBase.config_parse_yaml("""
# if no version is specified then version 1 is presumed
version: 1

# Invalid URL definition; yet the answer to life at the same time
urls: 43
""", asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # Invalid url/schema
    result, config = ConfigBase.config_parse_yaml("""
# if no version is specified then version 1 is presumed
version: 1

urls:
  - invalid://

""", asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # Invalid url/schema
    result, config = ConfigBase.config_parse_yaml("""
# if no version is specified then version 1 is presumed
version: 1

urls:
  - invalid://:
    - a: b

""", asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # Invalid url/schema
    result, config = ConfigBase.config_parse_yaml("""
# Include entry with nothing associated with it
include:

urls:
  - just some free text that isn't valid:
    - a garbage entry to go with it

""", asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # Invalid url/schema
    result, config = ConfigBase.config_parse_yaml("""
# if no version is specified then version 1 is presumed
version: 1

urls:
  - not even a proper url

""", asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # Invalid url/schema
    result, config = ConfigBase.config_parse_yaml("""
urls:
  # a very invalid sns entry
  - sns://T1JJ3T3L2/
  - sns://:@/:
    - invalid: test
  - sns://T1JJ3T3L2/:
    - invalid: test
    - _invalid: Token can not start with an underscore

  # some strangeness
  -
    -
      - test

""", asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # Valid Configuration
    result, config = ConfigBase.config_parse_yaml("""
# if no version is specified then version 1 is presumed
version: 1

# Including by dict
include:
  # File includes
  - file:///absolute/path/
  - relative/path
  # Trailing colon shouldn't disrupt include
  - http://test.com:

  # invalid (numeric)
  - 4

  # some strangeness
  -
    -
      - test

#
# Define your notification urls:
#
urls:
  - pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b
  - mailto://test:password@gmail.com
  - https://apprise.ryver.com/application/webhook/ckhrjW8w672m6HG
  - https://not.a.native.url/

    # A completely invalid token on json string (it gets ignored)
    # but the URL is still valid
  - json://localhost?invalid-token=nodashes

""", asset=asset)

    # We expect to parse 4 entries from the above
    # The Ryver one is in a native form and the 4th one is invalid
    assert isinstance(result, list)
    assert len(result) == 4
    assert len(result[0].tags) == 0

    # There were 3 include entries
    assert len(config) == 3
    assert 'file:///absolute/path/' in config
    assert 'relative/path' in config
    assert 'http://test.com' in config

    # Valid Configuration
    result, config = ConfigBase.config_parse_yaml("""
# A single line include is supported
include: http://localhost:8080/notify/apprise

urls:
  # The following generates 1 service
  - json://localhost:
       tag: my-custom-tag, my-other-tag

  # The following also generates 1 service
  - json://localhost:
    - tag: my-custom-tag, my-other-tag

  # How to stack multiple entries (this generates 2):
  - mailto://user:123abc@yahoo.ca:
    - to: test@examle.com
    - to: test2@examle.com

      # This is an illegal entry; the schema can not be changed
      schema: json

  # accidently left a colon at the end of the url; no problem
  # we'll accept it
  - mailto://oscar:pass@gmail.com:

  # A Ryver URL (using Native format); still accepted
  - https://apprise.ryver.com/application/webhook/ckhrjW8w672m6HG:

  # An invalid URL with colon (ignored)
  - https://not.a.native.url/:

  # A telegram entry (returns a None in parse_url())
  - tgram://invalid

""", asset=asset)

    # We expect to parse 6 entries from the above because the tgram:// entry
    # would have failed to be loaded
    assert isinstance(result, list)
    assert len(result) == 6
    assert len(result[0].tags) == 2

    # Our single line included
    assert len(config) == 1
    assert 'http://localhost:8080/notify/apprise' in config

    # Global Tags
    result, config = ConfigBase.config_parse_yaml("""
# Global Tags stacked as a list
tag:
  - admin
  - devops

urls:
  - json://localhost
  - dbus://
""", asset=asset)

    # We expect to parse 2 entries from the above
    assert isinstance(result, list)
    assert len(result) == 2

    # There were no include entries defined
    assert len(config) == 0

    # all entries will have our global tags defined in them
    for entry in result:
        assert 'admin' in entry.tags
        assert 'devops' in entry.tags

    # Global Tags
    result, config = ConfigBase.config_parse_yaml("""
# Global Tags
tag: admin, devops

urls:
  # The following tags will get added to the global set
  - json://localhost:
    - tag: string-tag, my-other-tag, text

  # Tags can be presented in this list format too:
  - dbus://:
    - tag:
      - list-tag
      - dbus
""", asset=asset)

    # all entries will have our global tags defined in them
    for entry in result:
        assert 'admin' in entry.tags
        assert 'devops' in entry.tags

    # We expect to parse 2 entries from the above
    assert isinstance(result, list)
    assert len(result) == 2

    # json:// has 2 globals + 3 defined
    assert len(result[0].tags) == 5
    assert 'text' in result[0].tags

    # json:// has 2 globals + 2 defined
    assert len(result[1].tags) == 4
    assert 'list-tag' in result[1].tags

    # There were no include entries defined
    assert len(config) == 0

    # An invalid set of entries
    result, config = ConfigBase.config_parse_yaml("""
urls:
  # The following tags will get added to the global set
  - json://localhost:
    -
      -
        - entry
""", asset=asset)

    # We expect to parse 0 entries from the above
    assert isinstance(result, list)
    assert len(result) == 0

    # There were no include entries defined
    assert len(config) == 0

    # An asset we'll manipulate; set some system flags
    asset = AppriseAsset(_uid="abc123", _recursion=1)

    # Global Tags
    result, config = ConfigBase.config_parse_yaml("""
# Test the creation of our apprise asset object
asset:
  app_id: AppriseTest
  app_desc: Apprise Test Notifications
  app_url: http://nuxref.com
  async_mode: no

  # System flags should never get set
  _uid: custom_id
  _recursion: 100

  # Support setting empty values
  image_url_mask:
  image_url_logo:

  image_path_mask: tmp/path

  # invalid entry
  theme:
    -
      -
        - entry

  # Now for some invalid entries
  invalid: entry
  __init__: can't be over-ridden
  nolists:
    - we don't support these entries
    - in the apprise object

urls:
  - json://localhost:
""", asset=asset)

    # We expect to parse 1 entries from the above
    assert isinstance(result, list)
    assert len(result) == 1

    # There were no include entries defined
    assert len(config) == 0

    assert asset.app_id == "AppriseTest"
    assert asset.app_desc == "Apprise Test Notifications"
    assert asset.app_url == "http://nuxref.com"

    # Verify our system flags retain only the value they were initialized to
    assert asset._uid == "abc123"
    assert asset._recursion == 1

    # Boolean types stay boolean
    assert asset.async_mode is False

    # the theme was not updated and remains the same as it was
    assert asset.theme == AppriseAsset().theme

    # Empty string assignment
    assert isinstance(asset.image_url_mask, str)
    assert asset.image_url_mask == ""
    assert isinstance(asset.image_url_logo, str)
    assert asset.image_url_logo == ""

    # For on-lookers looking through this file; here is a perfectly formatted
    # YAML configuration file for your reference so you can see it without
    # all of the errors like the ones identified above
    result, config = ConfigBase.config_parse_yaml("""
# if no version is specified then version 1 is presumed. Thus this is a
# completely optional field. It's a good idea to just add this line because it
# will help with future ambiguity (if it ever occurs).
version: 1

# Define an Asset object if you wish (Optional)
asset:
  app_id: AppriseTest
  app_desc: Apprise Test Notifications
  app_url: http://nuxref.com

# Optionally define some global tags to associate with ALL of your
# urls below.
tag: admin, devops

# Define your URLs (Mandatory!)
urls:
  # Either on-line each entry like this:
  - json://localhost

  # Or add a colon to the end of the URL where you can optionally provide
  # over-ride entries.  One of the most likely entry to be used here
  # is the tag entry.  This gets extended to the global tag (if defined)
  # above
  - xml://localhost:
    - tag: customer

  # The more elements you specify under a URL the more times the URL will
  # get replicated and used. Hence this entry actually could be considered
  # 2 URLs being called with just the destination email address changed:
  - mailto://george:password@gmail.com:
     - to: jason@hotmail.com
     - to: fred@live.com

  # Again... to re-iterate, the above mailto:// would actually fire two (2)
  # separate emails each with a different destination address specified.
  # Be careful when defining your arguments and differentiating between
  # when to use the dash (-) and when not to.  Each time you do, you will
  # cause another instance to be created.

  # Defining more then 1 element to a muti-set is easy, it looks like this:
  - mailto://jackson:abc123@hotmail.com:
     - to: jeff@gmail.com
       tag: jeff, customer

     - to: chris@yahoo.com
       tag: chris, customer
""", asset=asset)

    # okay, here is how we get our total based on the above (read top-down)
    # +1  json:// entry
    # +1  xml:// entry
    # +2  mailto:// entry to jason@hotmail.com and fred@live.com
    # +2  mailto:// entry to jeff@gmail.com and chris@yahoo.com
    # = 6
    assert len(result) == 6

    # all six entries will have our global tags defined in them
    for entry in result:
        assert 'admin' in entry.tags
        assert 'devops' in entry.tags

    # Entries can be directly accessed as they were added

    # our json:// had no additional tags added; so just the global ones
    # So just 2; admin and devops (these were already validated above in the
    # for loop
    assert len(result[0].tags) == 2

    # our xml:// object has 1 tag added (customer)
    assert len(result[1].tags) == 3
    assert 'customer' in result[1].tags

    # You get the idea, here is just a direct mapping to the remaining entries
    # in the same order they appear above
    assert len(result[2].tags) == 2
    assert len(result[3].tags) == 2

    assert len(result[4].tags) == 4
    assert 'customer' in result[4].tags
    assert 'jeff' in result[4].tags

    assert len(result[5].tags) == 4
    assert 'customer' in result[5].tags
    assert 'chris' in result[5].tags

    # There were no include entries defined
    assert len(config) == 0

    # Valid Configuration (multi inline configuration entries)
    result, config = ConfigBase.config_parse_yaml("""
# A configuration file that contains 2 includes separated by a comma and/or
# space:
include: http://localhost:8080/notify/apprise, http://localhost/apprise/cfg

""", asset=asset)

    # We will have loaded no results
    assert isinstance(result, list)
    assert len(result) == 0

    # But our two configuration files will be present:
    assert len(config) == 2
    assert 'http://localhost:8080/notify/apprise' in config
    assert 'http://localhost/apprise/cfg' in config

    # Valid Configuration (another way of specifying more then one include)
    result, config = ConfigBase.config_parse_yaml("""
# A configuration file that contains 4 includes on their own
# lines beneath the keyword `include`:
include:
   http://localhost:8080/notify/apprise
   http://localhost/apprise/cfg01
   http://localhost/apprise/cfg02
   http://localhost/apprise/cfg03

""", asset=asset)

    # We will have loaded no results
    assert isinstance(result, list)
    assert len(result) == 0

    # But our 4 configuration files will be present:
    assert len(config) == 4
    assert 'http://localhost:8080/notify/apprise' in config
    assert 'http://localhost/apprise/cfg01' in config
    assert 'http://localhost/apprise/cfg02' in config
    assert 'http://localhost/apprise/cfg03' in config

    # Test a configuration with an invalid schema with options
    result, config = ConfigBase.config_parse_yaml("""
    urls:
      - invalid://:
          tag: 'invalid'
          :name: 'Testing2'
          :body: 'test body2'
          :title: 'test title2'
""", asset=asset)

    # We will have loaded no results
    assert isinstance(result, list)
    assert len(result) == 0

    # Valid Configuration (we allow comma separated entries for
    # each defined bullet)
    result, config = ConfigBase.config_parse_yaml("""
# A configuration file that contains 4 includes on their own
# lines beneath the keyword `include`:
include:
   - http://localhost:8080/notify/apprise, http://localhost/apprise/cfg01
     http://localhost/apprise/cfg02
   - http://localhost/apprise/cfg03

""", asset=asset)

    # We will have loaded no results
    assert isinstance(result, list)
    assert len(result) == 0

    # But our 4 configuration files will be present:
    assert len(config) == 4
    assert 'http://localhost:8080/notify/apprise' in config
    assert 'http://localhost/apprise/cfg01' in config
    assert 'http://localhost/apprise/cfg02' in config
    assert 'http://localhost/apprise/cfg03' in config


def test_yaml_vs_text_tagging():
    """
    API: ConfigBase YAML vs TEXT tagging
    """

    yaml_result, _ = ConfigBase.config_parse_yaml("""
    urls:
      - mailtos://lead2gold:yesqbrulvaelyxve@gmail.com:
         tag: mytag
    """)
    assert yaml_result

    text_result, _ = ConfigBase.config_parse_text("""
    mytag=mailtos://lead2gold:yesqbrulvaelyxve@gmail.com
    """)
    assert text_result

    # Now we compare our results and verify they are the same
    assert len(yaml_result) == len(text_result)
    assert isinstance(yaml_result[0], NotifyEmail)
    assert isinstance(text_result[0], NotifyEmail)
    assert 'mytag' in text_result[0]
    assert 'mytag' in yaml_result[0]


def test_config_base_config_tag_groups_yaml_01():
    """
    API: ConfigBase.config_tag_groups_yaml #1 object

    """

    # general reference used below
    asset = AppriseAsset()

    # Valid Configuration
    result, config = ConfigBase.config_parse_yaml("""
# if no version is specified then version 1 is presumed
version: 1

groups:
  - group1: tagB, tagC, tagNotAssigned
  - group2:
      - tagA
      - tagC
  - group3:
      - tagD: optional comment
      - tagA: optional comment #2

  # No assignment
  - group4

  # No assignment type 2
  - group5:

  # Integer assignment
  - group6: 3
  - group6: 3, 4, 5, test
  - group6: 3.5, tagC

  # Recursion
  - groupA: groupB
  - groupB: groupA
  # And Again... (just because)
  - groupA: groupB
  - groupB: groupA

  # Self assignment
  - groupX: groupX

  # Set up a larger recursive loop
  - groupG: groupH
  - groupH: groupI, groupJ
  - groupI: groupJ, groupG
  - groupJ: groupK, groupH, groupI
  - groupK: groupG

  # No tags assigned
  - groupK: ",,  , ,"
  - " , ": ",, , ,"

  # Multi Assignments
  - groupL, groupM: tagD, tagA
  - 4, groupN:
     - tagD
     - tagE, TagA

  # Add one more tag to groupL making it different then GroupM by 1
  - groupL: tagB
#
# Define your notification urls:
#
urls:
  - form://localhost:
     - tag: tagA
  - mailto://test:password@gmail.com:
     - tag: tagB
  - xml://localhost:
     - tag: tagC
  - json://localhost:
     - tag: tagD, tagA

""", asset=asset)

    # We expect to parse 4 entries from the above
    assert isinstance(result, list)
    assert isinstance(config, list)
    assert len(result) == 4

    # Our first element is our group tags
    assert len(result[0].tags) == 5
    assert 'group2' in result[0].tags
    assert 'group3' in result[0].tags
    assert 'groupL' in result[0].tags
    assert 'groupM' in result[0].tags
    assert 'tagA' in result[0].tags

    # No additional configuration is loaded
    assert len(config) == 0

    apobj = Apprise()
    assert apobj.add(result)
    # We match against 1 entry
    assert len([x for x in apobj.find('tagA')]) == 2
    assert len([x for x in apobj.find('tagB')]) == 1
    assert len([x for x in apobj.find('tagC')]) == 1
    assert len([x for x in apobj.find('tagD')]) == 1
    assert len([x for x in apobj.find('group1')]) == 2
    assert len([x for x in apobj.find('group2')]) == 3
    assert len([x for x in apobj.find('group3')]) == 2
    assert len([x for x in apobj.find('group4')]) == 0
    assert len([x for x in apobj.find('group5')]) == 0
    # json:// -- group6 -> 4 -> TagA
    # xml://  -- group6 -> TagC
    assert len([x for x in apobj.find('group6')]) == 2
    assert len([x for x in apobj.find('4')]) == 1
    assert len([x for x in apobj.find('groupN')]) == 1


def test_config_base_config_tag_groups_yaml_02():
    """
    API: ConfigBase.config_tag_groups_yaml #2 object

    """

    # general reference used below
    asset = AppriseAsset()

    # Valid Configuration
    result, config = ConfigBase.config_parse_yaml("""
# if no version is specified then version 1 is presumed
version: 1

groups:
  group1: tagB, tagC, tagNotAssigned
  group2:
    - tagA
    - tagC
  group3:
    - tagD: optional comment
    - tagA: optional comment #2

  # No assignment type 2
  group5:

  # Integer assignment (since it's not a list, the last element prevails
  # and replaces the above); '4' does not get appended as it would in
  # the event this was a list instead
  group6: 3
  group6: 3, 4, 5, test
  group6: 3.5, tagC

  # Recursion
  groupA: groupB
  groupB: groupA
  # And Again... (just because)
  groupA: groupB
  groupB: groupA

  # Self assignment
  groupX: groupX

  # Set up a larger recursive loop
  groupG: groupH
  groupH: groupI, groupJ
  groupI: groupJ, groupG
  groupJ: groupK, groupH, groupI
  groupK: groupG

  # No tags assigned
  groupK: ",,  , ,"
  " , ": ",, , ,"

  # Multi Assignments
  groupL, groupM: tagD, tagA
  4, groupN:
   - tagD
   - tagE, TagA

  # Add one more tag to groupL making it different then GroupM by 1
  groupL: tagB
#
# Define your notification urls:
#
urls:
  - form://localhost:
     - tag: tagA
  - mailto://test:password@gmail.com:
     - tag: tagB
  - xml://localhost:
     - tag: tagC
  - json://localhost:
     - tag: tagD, tagA

""", asset=asset)

    # We expect to parse 4 entries from the above
    assert isinstance(result, list)
    assert isinstance(config, list)
    assert len(result) == 4

    # Our first element is our group tags
    assert len(result[0].tags) == 5
    assert 'group2' in result[0].tags
    assert 'group3' in result[0].tags
    assert 'groupL' in result[0].tags
    assert 'groupM' in result[0].tags
    assert 'tagA' in result[0].tags

    # No additional configuration is loaded
    assert len(config) == 0

    apobj = Apprise()
    assert apobj.add(result)
    # We match against 1 entry
    assert len([x for x in apobj.find('tagA')]) == 2
    assert len([x for x in apobj.find('tagB')]) == 1
    assert len([x for x in apobj.find('tagC')]) == 1
    assert len([x for x in apobj.find('tagD')]) == 1
    assert len([x for x in apobj.find('group1')]) == 2
    assert len([x for x in apobj.find('group2')]) == 3
    assert len([x for x in apobj.find('group3')]) == 2
    assert len([x for x in apobj.find('group4')]) == 0
    assert len([x for x in apobj.find('group5')]) == 0
    # NOT json:// -- group6 -> 4 -> TagA (not appended because dict storage)
    #                          ^
    #                          |
    #            See: test_config_base_config_tag_groups_yaml_01 (above)
    #                 dict storage (as this tests for) causes last entry to
    #                 prevail; previous assignments are lost
    #
    # xml://  -- group6 -> TagC
    assert len([x for x in apobj.find('group6')]) == 1
    assert len([x for x in apobj.find('4')]) == 1
    assert len([x for x in apobj.find('groupN')]) == 1
    assert len([x for x in apobj.find('groupK')]) == 0


def test_config_base_config_parse_yaml_globals():
    """
    API: ConfigBase.config_parse_yaml globals

    """

    # general reference used below
    asset = AppriseAsset()

    # Invalid Syntax (throws a ScannerError)
    results, config = ConfigBase.config_parse_yaml(cleandoc("""
    urls:
      - jsons://localhost1:
         - to: jeff@gmail.com
           tag: jeff, customer
           cto: 30
           rto: 30
           verify: no

      - jsons://localhost2?cto=30&rto=30&verify=no:
         - to: json@gmail.com
           tag: json, customer
    """), asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(results, list)

    # Our results loaded
    assert len(results) == 2
    assert len(config) == 0

    # Now verify that our global variables correctly initialized
    for entry in results:
        assert entry.verify_certificate is False
        assert entry.socket_read_timeout == 30
        assert entry.socket_connect_timeout == 30


# This test fails on CentOS 8.x so it was moved into it's own function
# so it could be bypassed. The ability to use lists in YAML files didn't
# appear to happen until later on; it's certainly not available in v3.12
# which was what shipped with CentOS v8 at the time.
@pytest.mark.skipif(int(yaml.__version__.split('.')[0]) <= 3,
                    reason="requires pyaml v4.x or higher.")
def test_config_base_config_parse_yaml_list():
    """
    API: ConfigBase.config_parse_yaml list parsing

    """

    # general reference used below
    asset = AppriseAsset()

    # Invalid url/schema
    result, config = ConfigBase.config_parse_yaml("""
# no lists... just no
urls: [milk, pumpkin pie, eggs, juice]

# Including by list is okay
include: [file:///absolute/path/, relative/path, http://test.com]

""", asset=asset)

    # Invalid data gets us an empty result set
    assert isinstance(result, list)
    assert len(result) == 0

    # There were 3 include entries
    assert len(config) == 3
    assert 'file:///absolute/path/' in config
    assert 'relative/path' in config
    assert 'http://test.com' in config
