"""Test utils module"""

import os
import random
import string
import binascii
from unittest import TestCase
from unittest.mock import Mock
import pytest
from es_client.exceptions import ConfigurationError
from es_client import utils as u
from . import FileTestObj

# pylint: disable=R0903,W0718

DEFAULT = {
    "elasticsearch": {
        "other_settings": {
            "master_only": False,
            "skip_version_test": False,
            "username": None,
            "password": None,
        },
        "client": {
            "hosts": "http://127.0.0.1:9200",
            "request_timeout": 30,
        },
    }
}


# The leading spaces are important here to create a proper yaml file.
YAML = "\n".join(["---", "elasticsearch:", "  client:", "    hosts: {0}"])


def random_envvar(size):
    """Generate a random environment variable"""
    return "".join(
        random.SystemRandom().choice(string.ascii_uppercase + string.digits)
        for _ in range(size)
    )


class TestEnsureList(TestCase):
    """Test the u.ensure_list function"""

    def test_utils_ensure_list_returns_lists(self):
        """
        Test several examples of lists: existing lists, strings, mixed lists/numbers
        """
        verify = ["a", "b", "c", "d"]
        source = ["a", "b", "c", "d"]
        assert verify == u.ensure_list(source)
        verify = ["abcd"]
        source = "abcd"
        assert verify == u.ensure_list(source)
        verify = [["abcd", "defg"], 1, 2, 3]
        source = [["abcd", "defg"], 1, 2, 3]
        assert verify == u.ensure_list(source)
        verify = [{"a": "b", "c": "d"}]
        source = {"a": "b", "c": "d"}
        assert verify == u.ensure_list(source)


class TestPruneNones(TestCase):
    """Test the u.prune_nones function"""

    def test_utils_prune_nones_with(self):
        """Ensure that a dict with a single None value comes back as an empty dict"""
        assert not u.prune_nones({"a": None})

    def test_utils_prune_nones_without(self):
        """Ensure that a dict with no None values comes back unchanged"""
        testval = {"foo": "bar"}
        assert testval == u.prune_nones(testval)


class TestReadFile:
    """Test the u.read_file function"""

    def test_utils_read_file_present(self):
        """Ensure that the written value is what was in the filename"""
        obj = FileTestObj()
        assert obj.written_value == u.read_file(obj.args["filename"])
        obj.teardown()

    def test_raise_when_no_file(self):
        """Raise an exception when there is no file"""
        obj = FileTestObj()
        with pytest.raises(ConfigurationError):
            u.read_file(obj.args["no_file_here"])
        obj.teardown()


class TestReadCerts:
    """Test the u.verify_ssl_paths function"""

    def test_all_as_one(self):
        """Test all 3 possible cert files at once from the same file"""
        obj = FileTestObj()
        config = {
            "ca_certs": obj.args["filename"],
            "client_cert": obj.args["filename"],
            "client_key": obj.args["filename"],
        }
        try:
            u.verify_ssl_paths(config)
        except Exception:
            pytest.fail("Unexpected Exception...")


class TestEnvVars:
    """Test the ability to read environment variables"""

    def test_present(self):
        """Test an existing (present) envvar"""
        obj = FileTestObj()
        evar = random_envvar(8)
        os.environ[evar] = "1234"
        dollar = "${" + evar + "}"
        obj.write_config(obj.args["configfile"], YAML.format(dollar))
        cfg = u.get_yaml(obj.args["configfile"])
        assert cfg["elasticsearch"]["client"]["hosts"] == os.environ.get(evar)
        del os.environ[evar]
        obj.teardown()

    def test_not_present(self):
        """Test a non-existent (not-present) envvar. It should set None here"""
        obj = FileTestObj()
        evar = random_envvar(8)
        dollar = "${" + evar + "}"
        obj.write_config(obj.args["configfile"], YAML.format(dollar))
        cfg = u.get_yaml(obj.args["configfile"])
        assert cfg["elasticsearch"]["client"]["hosts"] is None
        obj.teardown()

    def test_not_present_with_default(self):
        """
        Test a non-existent (not-present) envvar. It should set a default value here
        """
        obj = FileTestObj()
        evar = random_envvar(8)
        default = random_envvar(8)
        dollar = "${" + evar + ":" + default + "}"
        obj.write_config(obj.args["configfile"], YAML.format(dollar))
        cfg = u.get_yaml(obj.args["configfile"])
        assert cfg["elasticsearch"]["client"]["hosts"] == default
        obj.teardown()

    def test_raises_exception(self):
        """Ensure that improper formatting raises a ConfigurationError exception"""
        obj = FileTestObj()
        obj.write_config(
            obj.args["configfile"],
            """
            [weird brackets go here]
            I'm not a yaml file!!!=I have no keys
            I have lots of spaces
            """,
        )
        with pytest.raises(ConfigurationError):
            u.get_yaml(obj.args["configfile"])
        obj.teardown()


class TestVerifyURLSchema:
    """Test the u.verify_url_schema function"""

    def test_full_schema(self):
        """Verify that a proper schema comes back in normalized format"""
        url = "https://127.0.0.1:9200"
        assert u.verify_url_schema(url) == "https://127.0.0.1:9200"

    def test_http_schema_no_port(self):
        """
        Verify that port 80 is added when no port is specified for http
        """
        url = "http://127.0.0.1"
        assert u.verify_url_schema(url) == "http://127.0.0.1:80"

    def test_https_schema_no_port(self):
        """
        Verify that port 443 is added when no port is specified for https
        """
        url = "https://127.0.0.1"
        assert u.verify_url_schema(url) == "https://127.0.0.1:443"

    def test_http_with_path(self):
        """Verify that URLs with paths are accepted and normalized"""
        url = "http://localhost:9200/api/v1"
        assert u.verify_url_schema(url) == "http://localhost:9200"

    def test_https_with_path(self):
        """Verify that HTTPS URLs with paths are accepted and normalized"""
        url = "https://example.com/path/to/resource"
        assert u.verify_url_schema(url) == "https://example.com:443"

    def test_http_no_port_with_path(self):
        """Verify that HTTP URLs without ports but with paths work"""
        url = "http://localhost/api/v1/search"
        assert u.verify_url_schema(url) == "http://localhost:80"

    def test_bad_schema_no_port(self):
        """A URL starting with other than http or https raises an exception w/o port"""
        url = "ftp://127.0.0.1"
        with pytest.raises(ConfigurationError):
            u.verify_url_schema(url)

    def test_bad_schema_with_port(self):
        """A URL starting with other than http or https raises an exception w/port"""
        url = "ftp://127.0.0.1:1234"
        with pytest.raises(ConfigurationError):
            u.verify_url_schema(url)

    def test_invalid_hostname(self):
        """A URL without a valid hostname raises an exception"""
        url = "http://"
        with pytest.raises(ConfigurationError):
            u.verify_url_schema(url)


class TestGetVersion:
    """Test the u.get_version function"""

    def test_positive(self):
        """Ensure that what goes in comes back out unchanged"""
        client = Mock()
        client.info.return_value = {"version": {"number": "9.9.9"}}
        version = u.get_version(client)
        assert version == (9, 9, 9)

    def test_negative(self):
        """Ensure that mismatches are caught"""
        client = Mock()
        client.info.return_value = {"version": {"number": "9.9.9"}}
        version = u.get_version(client)
        assert version != (8, 8, 8)

    def test_dev_version_4_dots(self):
        """Test that anything after a third value and a period is truncated"""
        client = Mock()
        client.info.return_value = {"version": {"number": "9.9.9.dev"}}
        version = u.get_version(client)
        assert version == (9, 9, 9)

    def test_dev_version_with_dash(self):
        """Test that anything after a third value and a dash is truncated"""
        client = Mock()
        client.info.return_value = {"version": {"number": "9.9.9-dev"}}
        version = u.get_version(client)
        assert version == (9, 9, 9)


class TestFileExists:
    """Test the u.file_exists function"""

    def test_positive(self):
        """Ensure that an existing file returns True"""
        obj = FileTestObj()
        obj.write_config(
            obj.args["configfile"],
            """
            [weird brackets go here]
            I'm not a yaml file!!!=I have no keys
            I have lots of spaces
            """,
        )
        assert u.file_exists(obj.args["configfile"])
        obj.teardown()

    def test_negative(self):
        """Ensure that a non-existing file returns False"""
        obj = FileTestObj()
        obj.write_config(
            obj.args["configfile"],
            """
            This file will be deleted
            """,
        )
        obj.teardown()
        assert not u.file_exists(obj.args["configfile"])


class TestParseAPIKeyToken:
    """Test the u.parse_apikey_token function"""

    def success(self):
        """Successfully parse a token"""
        token = "X1VoN0VZY0JJV0lrUTlrdS1QZ2k6QjNZN1VJMlVRd0NHM1VTdHhuNnRKdw=="
        expected = ("_Uh7EYcBIWIkQ9ku-Pgi", "B3Y7UI2UQwCG3UStxn6tJw")
        assert expected == u.parse_apikey_token(token)

    def raises_exception1(self):
        """Raise a binascii.Error when unable to base64 decode a token"""
        token = "Not a valid token"
        with pytest.raises(binascii.Error):
            u.parse_apikey_token(token)

    def raises_exception2(self):
        """Raise an IndexError when able to base64 decode a token, not split by colon"""
        token = "VGhpcyB0ZXh0IGhhcyBubyBjb2xvbg=="
        with pytest.raises(IndexError):
            u.parse_apikey_token(token)
