# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import pytest


class APIErrorMock(Exception):
    def __init__(self, message, response=None, explanation=None):
        self.message = message
        self.response = response
        self.explanation = explanation


@pytest.fixture(autouse=True)
def docker_module_mock(mocker):
    docker_module_mock = mocker.MagicMock()
    docker_utils_module_mock = mocker.MagicMock()
    docker_errors_module_mock = mocker.MagicMock()
    docker_errors_module_mock.APIError = APIErrorMock
    mock_modules = {
        'docker': docker_module_mock,
        'docker.utils': docker_utils_module_mock,
        'docker.errors': docker_errors_module_mock,
    }
    return mocker.patch.dict('sys.modules', **mock_modules)


@pytest.fixture(autouse=True)
def docker_swarm_service():
    from ansible_collections.community.docker.plugins.modules import docker_swarm_service

    return docker_swarm_service


def test_retry_on_out_of_sequence_error(mocker, docker_swarm_service):
    run_mock = mocker.MagicMock(
        side_effect=APIErrorMock(
            message='',
            response=None,
            explanation='rpc error: code = Unknown desc = update out of sequence',
        )
    )
    manager = docker_swarm_service.DockerServiceManager(client=None)
    manager.run = run_mock
    with pytest.raises(APIErrorMock):
        manager.run_safe()
    assert run_mock.call_count == 3


def test_no_retry_on_general_api_error(mocker, docker_swarm_service):
    run_mock = mocker.MagicMock(
        side_effect=APIErrorMock(message='', response=None, explanation='some error')
    )
    manager = docker_swarm_service.DockerServiceManager(client=None)
    manager.run = run_mock
    with pytest.raises(APIErrorMock):
        manager.run_safe()
    assert run_mock.call_count == 1


def test_get_docker_environment(mocker, docker_swarm_service):
    env_file_result = {'TEST1': 'A', 'TEST2': 'B', 'TEST3': 'C'}
    env_dict = {'TEST3': 'CC', 'TEST4': 'D'}
    env_string = "TEST3=CC,TEST4=D"

    env_list = ['TEST3=CC', 'TEST4=D']
    expected_result = sorted(['TEST1=A', 'TEST2=B', 'TEST3=CC', 'TEST4=D'])
    mocker.patch.object(
        docker_swarm_service, 'parse_env_file', return_value=env_file_result
    )
    mocker.patch.object(
        docker_swarm_service,
        'format_environment',
        side_effect=lambda d: ['{0}={1}'.format(key, value) for key, value in d.items()],
    )
    # Test with env dict and file
    result = docker_swarm_service.get_docker_environment(
        env_dict, env_files=['dummypath']
    )
    assert result == expected_result
    # Test with env list and file
    result = docker_swarm_service.get_docker_environment(
        env_list,
        env_files=['dummypath']
    )
    assert result == expected_result
    # Test with env string and file
    result = docker_swarm_service.get_docker_environment(
        env_string, env_files=['dummypath']
    )
    assert result == expected_result

    assert result == expected_result
    # Test with empty env
    result = docker_swarm_service.get_docker_environment(
        [], env_files=None
    )
    assert result == []
    # Test with empty env_files
    result = docker_swarm_service.get_docker_environment(
        None, env_files=[]
    )
    assert result == []


def test_get_nanoseconds_from_raw_option(docker_swarm_service):
    value = docker_swarm_service.get_nanoseconds_from_raw_option('test', None)
    assert value is None

    value = docker_swarm_service.get_nanoseconds_from_raw_option('test', '1m30s535ms')
    assert value == 90535000000

    value = docker_swarm_service.get_nanoseconds_from_raw_option('test', 10000000000)
    assert value == 10000000000

    with pytest.raises(ValueError):
        docker_swarm_service.get_nanoseconds_from_raw_option('test', [])


def test_has_dict_changed(docker_swarm_service):
    assert not docker_swarm_service.has_dict_changed(
        {"a": 1},
        {"a": 1},
    )
    assert not docker_swarm_service.has_dict_changed(
        {"a": 1},
        {"a": 1, "b": 2}
    )
    assert docker_swarm_service.has_dict_changed(
        {"a": 1},
        {"a": 2, "b": 2}
    )
    assert docker_swarm_service.has_dict_changed(
        {"a": 1, "b": 1},
        {"a": 1}
    )
    assert not docker_swarm_service.has_dict_changed(
        None,
        {"a": 2, "b": 2}
    )
    assert docker_swarm_service.has_dict_changed(
        {},
        {"a": 2, "b": 2}
    )
    assert docker_swarm_service.has_dict_changed(
        {"a": 1},
        {}
    )
    assert docker_swarm_service.has_dict_changed(
        {"a": 1},
        None
    )
    assert not docker_swarm_service.has_dict_changed(
        {},
        {}
    )
    assert not docker_swarm_service.has_dict_changed(
        None,
        None
    )
    assert not docker_swarm_service.has_dict_changed(
        {},
        None
    )
    assert not docker_swarm_service.has_dict_changed(
        None,
        {}
    )


def test_has_list_changed(docker_swarm_service):

    # List comparisons without dictionaries
    # I could improve the indenting, but pycodestyle wants this instead
    assert not docker_swarm_service.has_list_changed(None, None)
    assert not docker_swarm_service.has_list_changed(None, [])
    assert not docker_swarm_service.has_list_changed(None, [1, 2])

    assert not docker_swarm_service.has_list_changed([], None)
    assert not docker_swarm_service.has_list_changed([], [])
    assert docker_swarm_service.has_list_changed([], [1, 2])

    assert docker_swarm_service.has_list_changed([1, 2], None)
    assert docker_swarm_service.has_list_changed([1, 2], [])

    assert docker_swarm_service.has_list_changed([1, 2, 3], [1, 2])
    assert docker_swarm_service.has_list_changed([1, 2], [1, 2, 3])

    # Check list sorting
    assert not docker_swarm_service.has_list_changed([1, 2], [2, 1])
    assert docker_swarm_service.has_list_changed(
        [1, 2],
        [2, 1],
        sort_lists=False
    )

    # Check type matching
    assert docker_swarm_service.has_list_changed([None, 1], [2, 1])
    assert docker_swarm_service.has_list_changed([2, 1], [None, 1])
    assert docker_swarm_service.has_list_changed(
        "command --with args",
        ['command', '--with', 'args']
    )
    assert docker_swarm_service.has_list_changed(
        ['sleep', '3400'],
        [u'sleep', u'3600'],
        sort_lists=False
    )

    # List comparisons with dictionaries
    assert not docker_swarm_service.has_list_changed(
        [{'a': 1}],
        [{'a': 1}],
        sort_key='a'
    )

    assert not docker_swarm_service.has_list_changed(
        [{'a': 1}, {'a': 2}],
        [{'a': 1}, {'a': 2}],
        sort_key='a'
    )

    with pytest.raises(Exception):
        docker_swarm_service.has_list_changed(
            [{'a': 1}, {'a': 2}],
            [{'a': 1}, {'a': 2}]
        )

    # List sort checking with sort key
    assert not docker_swarm_service.has_list_changed(
        [{'a': 1}, {'a': 2}],
        [{'a': 2}, {'a': 1}],
        sort_key='a'
    )
    assert docker_swarm_service.has_list_changed(
        [{'a': 1}, {'a': 2}],
        [{'a': 2}, {'a': 1}],
        sort_lists=False
    )

    assert docker_swarm_service.has_list_changed(
        [{'a': 1}, {'a': 2}, {'a': 3}],
        [{'a': 2}, {'a': 1}],
        sort_key='a'
    )
    assert docker_swarm_service.has_list_changed(
        [{'a': 1}, {'a': 2}],
        [{'a': 1}, {'a': 2}, {'a': 3}],
        sort_lists=False
    )

    # Additional dictionary elements
    assert not docker_swarm_service.has_list_changed(
        [
            {"src": 1, "dst": 2},
            {"src": 1, "dst": 2, "protocol": "udp"},
        ],
        [
            {"src": 1, "dst": 2, "protocol": "tcp"},
            {"src": 1, "dst": 2, "protocol": "udp"},
        ],
        sort_key='dst'
    )
    assert not docker_swarm_service.has_list_changed(
        [
            {"src": 1, "dst": 2, "protocol": "udp"},
            {"src": 1, "dst": 3, "protocol": "tcp"},
        ],
        [
            {"src": 1, "dst": 2, "protocol": "udp"},
            {"src": 1, "dst": 3, "protocol": "tcp"},
        ],
        sort_key='dst'
    )
    assert docker_swarm_service.has_list_changed(
        [
            {"src": 1, "dst": 2, "protocol": "udp"},
            {"src": 1, "dst": 2},
            {"src": 3, "dst": 4},
        ],
        [
            {"src": 1, "dst": 3, "protocol": "udp"},
            {"src": 1, "dst": 2, "protocol": "tcp"},
            {"src": 3, "dst": 4, "protocol": "tcp"},
        ],
        sort_key='dst'
    )
    assert docker_swarm_service.has_list_changed(
        [
            {"src": 1, "dst": 3, "protocol": "tcp"},
            {"src": 1, "dst": 2, "protocol": "udp"},
        ],
        [
            {"src": 1, "dst": 2, "protocol": "tcp"},
            {"src": 1, "dst": 2, "protocol": "udp"},
        ],
        sort_key='dst'
    )
    assert docker_swarm_service.has_list_changed(
        [
            {"src": 1, "dst": 2, "protocol": "udp"},
            {"src": 1, "dst": 2, "protocol": "tcp", "extra": {"test": "foo"}},
        ],
        [
            {"src": 1, "dst": 2, "protocol": "udp"},
            {"src": 1, "dst": 2, "protocol": "tcp"},
        ],
        sort_key='dst'
    )
    assert not docker_swarm_service.has_list_changed(
        [{'id': '123', 'aliases': []}],
        [{'id': '123'}],
        sort_key='id'
    )


def test_have_networks_changed(docker_swarm_service):
    assert not docker_swarm_service.have_networks_changed(
        None,
        None
    )

    assert not docker_swarm_service.have_networks_changed(
        [],
        None
    )

    assert not docker_swarm_service.have_networks_changed(
        [{'id': 1}],
        [{'id': 1}]
    )

    assert docker_swarm_service.have_networks_changed(
        [{'id': 1}],
        [{'id': 1}, {'id': 2}]
    )

    assert not docker_swarm_service.have_networks_changed(
        [{'id': 1}, {'id': 2}],
        [{'id': 1}, {'id': 2}]
    )

    assert not docker_swarm_service.have_networks_changed(
        [{'id': 1}, {'id': 2}],
        [{'id': 2}, {'id': 1}]
    )

    assert not docker_swarm_service.have_networks_changed(
        [
            {'id': 1},
            {'id': 2, 'aliases': []}
        ],
        [
            {'id': 1},
            {'id': 2}
        ]
    )

    assert docker_swarm_service.have_networks_changed(
        [
            {'id': 1},
            {'id': 2, 'aliases': ['alias1']}
        ],
        [
            {'id': 1},
            {'id': 2}
        ]
    )

    assert docker_swarm_service.have_networks_changed(
        [
            {'id': 1},
            {'id': 2, 'aliases': ['alias1', 'alias2']}
        ],
        [
            {'id': 1},
            {'id': 2, 'aliases': ['alias1']}
        ]
    )

    assert not docker_swarm_service.have_networks_changed(
        [
            {'id': 1},
            {'id': 2, 'aliases': ['alias1', 'alias2']}
        ],
        [
            {'id': 1},
            {'id': 2, 'aliases': ['alias1', 'alias2']}
        ]
    )

    assert not docker_swarm_service.have_networks_changed(
        [
            {'id': 1},
            {'id': 2, 'aliases': ['alias1', 'alias2']}
        ],
        [
            {'id': 1},
            {'id': 2, 'aliases': ['alias2', 'alias1']}
        ]
    )

    assert not docker_swarm_service.have_networks_changed(
        [
            {'id': 1, 'options': {}},
            {'id': 2, 'aliases': ['alias1', 'alias2']}],
        [
            {'id': 1},
            {'id': 2, 'aliases': ['alias2', 'alias1']}
        ]
    )

    assert not docker_swarm_service.have_networks_changed(
        [
            {'id': 1, 'options': {'option1': 'value1'}},
            {'id': 2, 'aliases': ['alias1', 'alias2']}],
        [
            {'id': 1, 'options': {'option1': 'value1'}},
            {'id': 2, 'aliases': ['alias2', 'alias1']}
        ]
    )

    assert docker_swarm_service.have_networks_changed(
        [
            {'id': 1, 'options': {'option1': 'value1'}},
            {'id': 2, 'aliases': ['alias1', 'alias2']}],
        [
            {'id': 1, 'options': {'option1': 'value2'}},
            {'id': 2, 'aliases': ['alias2', 'alias1']}
        ]
    )


def test_get_docker_networks(docker_swarm_service):
    network_names = [
        'network_1',
        'network_2',
        'network_3',
        'network_4',
    ]
    networks = [
        network_names[0],
        {'name': network_names[1]},
        {'name': network_names[2], 'aliases': ['networkalias1']},
        {'name': network_names[3], 'aliases': ['networkalias2'], 'options': {'foo': 'bar'}},
    ]
    network_ids = {
        network_names[0]: '1',
        network_names[1]: '2',
        network_names[2]: '3',
        network_names[3]: '4',
    }
    parsed_networks = docker_swarm_service.get_docker_networks(
        networks,
        network_ids
    )
    assert len(parsed_networks) == 4
    for i, network in enumerate(parsed_networks):
        assert 'name' not in network
        assert 'id' in network
        expected_name = network_names[i]
        assert network['id'] == network_ids[expected_name]
        if i == 2:
            assert network['aliases'] == ['networkalias1']
        if i == 3:
            assert network['aliases'] == ['networkalias2']
        if i == 3:
            assert 'foo' in network['options']
    # Test missing name
    with pytest.raises(TypeError):
        docker_swarm_service.get_docker_networks([{'invalid': 'err'}], {'err': 1})
    # test for invalid aliases type
    with pytest.raises(TypeError):
        docker_swarm_service.get_docker_networks(
            [{'name': 'test', 'aliases': 1}],
            {'test': 1}
        )
    # Test invalid aliases elements
    with pytest.raises(TypeError):
        docker_swarm_service.get_docker_networks(
            [{'name': 'test', 'aliases': [1]}],
            {'test': 1}
        )
    # Test for invalid options type
    with pytest.raises(TypeError):
        docker_swarm_service.get_docker_networks(
            [{'name': 'test', 'options': 1}],
            {'test': 1}
        )
    # Test for invalid networks type
    with pytest.raises(TypeError):
        docker_swarm_service.get_docker_networks(
            1,
            {'test': 1}
        )
    # Test for non existing networks
    with pytest.raises(ValueError):
        docker_swarm_service.get_docker_networks(
            [{'name': 'idontexist'}],
            {'test': 1}
        )
    # Test empty values
    assert docker_swarm_service.get_docker_networks([], {}) == []
    assert docker_swarm_service.get_docker_networks(None, {}) is None
    # Test invalid options
    with pytest.raises(TypeError):
        docker_swarm_service.get_docker_networks(
            [{'name': 'test', 'nonexisting_option': 'foo'}],
            {'test': '1'}
        )
