# 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 inspect import cleandoc
import json

# Disable logging for a cleaner testing output
import logging
from os.path import dirname, getsize, join
from unittest import mock

import pytest
import requests

from apprise import Apprise, AppriseAsset, AttachmentManager
from apprise.apprise_attachment import AppriseAttachment
from apprise.attachment import AttachBase
from apprise.common import ContentLocation

logging.disable(logging.CRITICAL)

TEST_VAR_DIR = join(dirname(__file__), "var")

# Grant access to our Attachment Manager Singleton
A_MGR = AttachmentManager()


def test_apprise_attachment():
    """
    API: AppriseAttachment basic testing

    """

    # Create ourselves an attachment object
    aa = AppriseAttachment()

    # There are no attachents loaded
    assert len(aa) == 0

    # Object can be directly checked as a boolean; response is False
    # when there are no entries loaded
    assert not aa

    # An attachment object using a custom Apprise Asset object
    # Set a cache expiry of 5 minutes (300 seconds)
    aa = AppriseAttachment(asset=AppriseAsset(), cache=300)

    # still no attachments added
    assert len(aa) == 0

    # Add a file by it's path
    path = join(TEST_VAR_DIR, "apprise-test.gif")
    assert aa.add(path)

    # There is now 1 attachment
    assert len(aa) == 1

    # our attachment took on our cache value
    assert aa[0].cache == 300

    # we can test the object as a boolean and get a value of True now
    assert aa

    # Add another entry already in it's AttachBase format
    response = AppriseAttachment.instantiate(path, cache=True)
    assert isinstance(response, AttachBase)
    assert aa.add(response, asset=AppriseAsset())

    # There is now 2 attachments
    assert len(aa) == 2

    # Cache is initialized to True
    assert aa[1].cache is True

    # Reset our object
    aa = AppriseAttachment()

    # We can add by lists as well in a variety of formats
    attachments = (
        path,
        f"file://{path}?name=newfilename.gif?cache=120",
        AppriseAttachment.instantiate(
            f"file://{path}?name=anotherfilename.gif", cache=100
        ),
    )

    # Add them
    assert aa.add(attachments, cache=False)

    # There is now 3 attachments
    assert len(aa) == 3

    # Take on our fixed cache value of False.
    # The last entry will have our set value of 100
    assert aa[0].cache is False
    # Even though we set a value of 120, we take on the value of False because
    # it was forced on the instantiate call
    assert aa[1].cache is False
    assert aa[2].cache == 100

    # We can pop the last element off of the list as well
    attachment = aa.pop()
    assert isinstance(attachment, AttachBase)
    # we can test of the attachment is valid using a boolean check:
    assert attachment
    assert len(aa) == 2
    assert attachment.path == path
    assert attachment.name == "anotherfilename.gif"
    assert attachment.mimetype == "image/gif"

    # elements can also be directly indexed
    assert isinstance(aa[0], AttachBase)
    assert isinstance(aa[1], AttachBase)

    with pytest.raises(IndexError):
        aa[2]

    # We can iterate over attachments too:
    for count, a in enumerate(aa):
        assert isinstance(a, AttachBase)

        # we'll never iterate more then the number of entries in our object
        assert count < len(aa)

    # Get the file-size of our image
    expected_size = getsize(path) * len(aa)

    # verify that's what we get as a result
    assert aa.size() == expected_size

    # Attachments can also be loaded during the instantiation of the
    # AppriseAttachment object
    aa = AppriseAttachment(attachments)

    # There is now 3 attachments
    assert len(aa) == 3

    # Reset our object
    aa.clear()
    assert len(aa) == 0
    assert not aa

    assert aa.add(
        AppriseAttachment.instantiate(
            f"file://{path}?name=andanother.png&cache=Yes"
        )
    )
    assert aa.add(
        AppriseAttachment.instantiate(
            f"file://{path}?name=andanother.png&cache=No"
        )
    )
    AppriseAttachment.instantiate(
        f"file://{path}?name=andanother.png&cache=600"
    )
    assert aa.add(
        AppriseAttachment.instantiate(
            f"file://{path}?name=andanother.png&cache=600"
        )
    )

    assert len(aa) == 3
    assert aa[0].cache is True
    assert aa[1].cache is False
    assert aa[2].cache == 600

    # Negative cache are not allowed
    assert not aa.add(
        AppriseAttachment.instantiate(
            f"file://{path}?name=andanother.png&cache=-600"
        )
    )

    # Invalid cache value
    assert not aa.add(
        AppriseAttachment.instantiate(
            f"file://{path}?name=andanother.png", cache="invalid"
        )
    )

    # No length change
    assert len(aa) == 3

    # Reset our object
    aa.clear()

    # Garbage in produces garbage out
    assert aa.add(None) is False
    assert aa.add(object()) is False
    assert aa.add(42) is False

    # length remains unchanged
    assert len(aa) == 0

    # We can add by lists as well in a variety of formats
    attachments = (
        None,
        object(),
        42,
        "garbage://",
    )

    # Add our attachments
    assert aa.add(attachments) is False

    # length remains unchanged
    assert len(aa) == 0

    # if instantiating attachments from the class, it will throw a TypeError
    # if attachments couldn't be loaded
    with pytest.raises(TypeError):
        AppriseAttachment("garbage://")

    # Load our other attachment types
    aa = AppriseAttachment(location=ContentLocation.LOCAL)

    # Hosted type won't allow us to import files
    aa = AppriseAttachment(location=ContentLocation.HOSTED)
    assert len(aa) == 0

    # Add our attachments defined a the head of this function
    aa.add(attachments)

    # Our length is still zero because we can't import files in
    # a hosted environment
    assert len(aa) == 0

    # Inaccessible type prevents the adding of new stuff
    aa = AppriseAttachment(location=ContentLocation.INACCESSIBLE)
    assert len(aa) == 0

    # Add our attachments defined a the head of this function
    aa.add(attachments)

    # Our length is still zero
    assert len(aa) == 0

    with pytest.raises(TypeError):
        # Invalid location specified
        AppriseAttachment(location="invalid")

    # test cases when file simply doesn't exist
    aa = AppriseAttachment("file://non-existant-file.png")
    # Our length is still 1
    assert len(aa) == 1
    # Our object will still return a True
    assert aa

    # However our indexed entry will not
    assert not aa[0]

    # length will return 0
    assert len(aa[0]) == 0

    # Total length will also return 0
    assert aa.size() == 0


@mock.patch("requests.request")
def test_apprise_attachment_truncate(mock_request):
    """
    API: AppriseAttachment when truncation in place

    """

    # Prepare our response
    response = requests.Request()
    response.status_code = requests.codes.ok

    # Prepare Mock
    mock_request.return_value = response

    # our Apprise Object
    ap_obj = Apprise()

    # Add ourselves an object set to truncate
    ap_obj.add("json://localhost/?method=GET&overflow=truncate")

    # Create ourselves an attachment object
    aa = AppriseAttachment()

    # There are no attachents loaded
    assert len(aa) == 0

    # Object can be directly checked as a boolean; response is False
    # when there are no entries loaded
    assert not aa

    # Add 2 attachments
    assert aa.add(join(TEST_VAR_DIR, "apprise-test.gif"))
    assert aa.add(join(TEST_VAR_DIR, "apprise-test.png"))

    assert mock_request.call_count == 0
    assert ap_obj.notify(body="body", title="title", attach=aa)

    assert mock_request.call_count == 1

    # Our first item was truncated, so only 1 attachment
    details = mock_request.call_args_list[0]
    assert details[0][0] == "GET"
    dataset = json.loads(details[1]["data"])
    assert len(dataset["attachments"]) == 1

    # Reset our object
    mock_request.reset_mock()

    # our Apprise Object
    ap_obj = Apprise()

    # Add ourselves an object set to upstream
    ap_obj.add("json://localhost/?method=GET&overflow=upstream")

    # Create ourselves an attachment object
    aa = AppriseAttachment()

    # Add 2 attachments
    assert aa.add(join(TEST_VAR_DIR, "apprise-test.gif"))
    assert aa.add(join(TEST_VAR_DIR, "apprise-test.png"))

    assert mock_request.call_count == 0
    assert ap_obj.notify(body="body", title="title", attach=aa)

    assert mock_request.call_count == 1

    # Our item was not truncated, so all attachments
    details = mock_request.call_args_list[0]
    assert details[0][0] == "GET"
    dataset = json.loads(details[1]["data"])
    assert len(dataset["attachments"]) == 2


def test_apprise_attachment_instantiate():
    """
    API: AppriseAttachment.instantiate()

    """
    assert (
        AppriseAttachment.instantiate("file://?", suppress_exceptions=True)
        is None
    )

    assert (
        AppriseAttachment.instantiate("invalid://?", suppress_exceptions=True)
        is None
    )

    class BadAttachType(AttachBase):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)

            # We fail whenever we're initialized
            raise TypeError()

    # Store our bad attachment type in our schema map
    A_MGR["bad"] = BadAttachType

    with pytest.raises(TypeError):
        AppriseAttachment.instantiate("bad://path", suppress_exceptions=False)

    # Same call but exceptions suppressed
    assert (
        AppriseAttachment.instantiate("bad://path", suppress_exceptions=True)
        is None
    )


def test_attachment_matrix_dynamic_importing(tmpdir):
    """
    API: Apprise() Attachment Matrix Importing

    """

    # Make our new path valid
    suite = tmpdir.mkdir("apprise_attach_test_suite")
    suite.join("__init__.py").write("")

    module_name = "badattach"

    # Create a base area to work within
    base = suite.mkdir(module_name)
    base.join("__init__.py").write("")

    # Test no app_id
    base.join("AttachBadFile1.py").write(
        cleandoc("""
        class AttachBadFile1:
            pass
        """)
    )

    # No class of the same name
    base.join("AttachBadFile2.py").write(
        cleandoc("""
        class BadClassName:
            pass
        """)
    )

    # Exception thrown
    base.join("AttachBadFile3.py").write("""raise ImportError()""")

    # Utilizes a schema:// already occupied (as string)
    base.join("AttachGoober.py").write(
        cleandoc("""
        from apprise import AttachBase
        class AttachGoober(AttachBase):
            # This class tests the fact we have a new class name, but we're
            # trying to over-ride items previously used

            # The default simple (insecure) protocol
            protocol = 'http'

            # The default secure protocol
            secure_protocol = 'https'
        """)
    )

    # Utilizes a schema:// already occupied (as tuple)
    base.join("AttachBugger.py").write(
        cleandoc("""
        from apprise import AttachBase
        class AttachBugger(AttachBase):
            # This class tests the fact we have a new class name, but we're
            # trying to over-ride items previously used

            # The default simple (insecure) protocol
            protocol = ('http', 'bugger-test' )

            # The default secure protocol
            secure_protocol = ('https', 'bugger-tests')
        """)
    )

    A_MGR.load_modules(path=str(base), name=module_name)
