"""Tests for flake8's utils module."""
from __future__ import annotations

import io
import logging
import os
import sys
from unittest import mock

import pytest

from flake8 import exceptions
from flake8 import utils

RELATIVE_PATHS = ["flake8", "pep8", "pyflakes", "mccabe"]


@pytest.mark.parametrize(
    "value,expected",
    [
        ("E123,\n\tW234,\n    E206", ["E123", "W234", "E206"]),
        ("E123,W234,E206", ["E123", "W234", "E206"]),
        ("E123 W234 E206", ["E123", "W234", "E206"]),
        ("E123\nW234 E206", ["E123", "W234", "E206"]),
        ("E123\nW234\nE206", ["E123", "W234", "E206"]),
        ("E123,W234,E206,", ["E123", "W234", "E206"]),
        ("E123,W234,E206, ,\n", ["E123", "W234", "E206"]),
        ("E123,W234,,E206,,", ["E123", "W234", "E206"]),
        ("E123, W234,, E206,,", ["E123", "W234", "E206"]),
        ("E123,,W234,,E206,,", ["E123", "W234", "E206"]),
        ("", []),
    ],
)
def test_parse_comma_separated_list(value, expected):
    """Verify that similar inputs produce identical outputs."""
    assert utils.parse_comma_separated_list(value) == expected


@pytest.mark.parametrize(
    ("value", "expected"),
    (
        # empty option configures nothing
        ("", []),
        ("   ", []),
        ("\n\n\n", []),
        # basic case
        (
            "f.py:E123",
            [("f.py", ["E123"])],
        ),
        # multiple filenames, multiple codes
        (
            "f.py,g.py:E,F",
            [("f.py", ["E", "F"]), ("g.py", ["E", "F"])],
        ),
        # demonstrate that whitespace is not important around tokens
        (
            "   f.py  , g.py  : E  , F  ",
            [("f.py", ["E", "F"]), ("g.py", ["E", "F"])],
        ),
        # whitespace can separate groups of configuration
        (
            "f.py:E g.py:F",
            [("f.py", ["E"]), ("g.py", ["F"])],
        ),
        # newlines can separate groups of configuration
        (
            "f.py: E\ng.py: F\n",
            [("f.py", ["E"]), ("g.py", ["F"])],
        ),
        # whitespace can be used in place of commas
        (
            "f.py g.py: E F",
            [("f.py", ["E", "F"]), ("g.py", ["E", "F"])],
        ),
        # go ahead, indent your codes
        (
            "f.py:\n    E,F\ng.py:\n    G,H",
            [("f.py", ["E", "F"]), ("g.py", ["G", "H"])],
        ),
        # capitalized filenames are ok too
        (
            "F.py,G.py: F,G",
            [("F.py", ["F", "G"]), ("G.py", ["F", "G"])],
        ),
        #  it's easier to allow zero filenames or zero codes than forbid it
        (":E", []),
        ("f.py:", []),
        (":E f.py:F", [("f.py", ["F"])]),
        ("f.py: g.py:F", [("g.py", ["F"])]),
        ("f.py:E:", []),
        ("f.py:E.py:", []),
        ("f.py:Eg.py:F", [("Eg.py", ["F"])]),
        # sequences are also valid (?)
        (
            ["f.py:E,F", "g.py:G,H"],
            [("f.py", ["E", "F"]), ("g.py", ["G", "H"])],
        ),
        # six-digits codes are allowed
        (
            "f.py: ABC123",
            [("f.py", ["ABC123"])],
        ),
    ),
)
def test_parse_files_to_codes_mapping(value, expected):
    """Test parsing of valid files-to-codes mappings."""
    assert utils.parse_files_to_codes_mapping(value) == expected


@pytest.mark.parametrize(
    "value",
    (
        # code while looking for filenames
        "E123",
        "f.py,E123",
        "f.py E123",
        # eof while looking for filenames
        "f.py",
        "f.py:E,g.py"
        # colon while looking for codes
        "f.py::",
        # no separator between
        "f.py:E1F1",
    ),
)
def test_invalid_file_list(value):
    """Test parsing of invalid files-to-codes mappings."""
    with pytest.raises(exceptions.ExecutionError):
        utils.parse_files_to_codes_mapping(value)


@pytest.mark.parametrize(
    "value,expected",
    [
        ("flake8", "flake8"),
        (".", os.path.abspath(".")),
        ("../flake8", os.path.abspath("../flake8")),
        ("flake8/", os.path.abspath("flake8")),
    ],
)
def test_normalize_path(value, expected):
    """Verify that we normalize paths provided to the tool."""
    assert utils.normalize_path(value) == expected


@pytest.mark.parametrize(
    "value,expected",
    [
        (
            ["flake8", "pep8", "pyflakes", "mccabe"],
            ["flake8", "pep8", "pyflakes", "mccabe"],
        ),
        (
            ["../flake8", "../pep8", "../pyflakes", "../mccabe"],
            [os.path.abspath(f"../{p}") for p in RELATIVE_PATHS],
        ),
    ],
)
def test_normalize_paths(value, expected):
    """Verify we normalizes a sequence of paths provided to the tool."""
    assert utils.normalize_paths(value) == expected


def test_matches_filename_for_excluding_dotfiles():
    """Verify that `.` and `..` are not matched by `.*`."""
    logger = logging.Logger(__name__)
    assert not utils.matches_filename(".", (".*",), "", logger)
    assert not utils.matches_filename("..", (".*",), "", logger)


@pytest.mark.parametrize(
    "filename,patterns,expected",
    [
        ("foo.py", [], True),
        ("foo.py", ["*.pyc"], False),
        ("foo.pyc", ["*.pyc"], True),
        ("foo.pyc", ["*.swp", "*.pyc", "*.py"], True),
    ],
)
def test_fnmatch(filename, patterns, expected):
    """Verify that our fnmatch wrapper works as expected."""
    assert utils.fnmatch(filename, patterns) is expected


def test_stdin_get_value_crlf():
    """Ensure that stdin is normalized from crlf to lf."""
    stdin = io.TextIOWrapper(io.BytesIO(b"1\r\n2\r\n"), "UTF-8")
    with mock.patch.object(sys, "stdin", stdin):
        assert utils.stdin_get_value.__wrapped__() == "1\n2\n"


def test_stdin_unknown_coding_token():
    """Ensure we produce source even for unknown encodings."""
    stdin = io.TextIOWrapper(io.BytesIO(b"# coding: unknown\n"), "UTF-8")
    with mock.patch.object(sys, "stdin", stdin):
        assert utils.stdin_get_value.__wrapped__() == "# coding: unknown\n"


@pytest.mark.parametrize(
    ("s", "expected"),
    (
        ("", ""),
        ("my-plugin", "my-plugin"),
        ("MyPlugin", "myplugin"),
        ("my_plugin", "my-plugin"),
        ("my.plugin", "my-plugin"),
        ("my--plugin", "my-plugin"),
        ("my__plugin", "my-plugin"),
    ),
)
def test_normalize_pypi_name(s, expected):
    assert utils.normalize_pypi_name(s) == expected
