import os
import re
from io import BytesIO
from itertools import chain
from types import SimpleNamespace

import pytest

from translate.convert import convert
from translate.storage import po

OPTION_RE = re.compile(r"^\s*-")


class TestConvertCommand:
    """Tests running actual commands on files."""

    convertmodule = convert
    defaultoptions = {"progress": "none"}
    expected_options = []

    def setup_method(self, method) -> None:
        """Creates a clean test directory for the given method."""
        self.testdir = f"{self.__class__.__name__}_{method.__name__}"
        self.cleardir()
        os.mkdir(self.testdir)
        self.rundir = os.path.abspath(os.getcwd())

    def teardown_method(self, method) -> None:
        """Removes the test directory for the given method."""
        os.chdir(self.rundir)
        self.cleardir()

    def cleardir(self) -> None:
        """Removes the test directory."""
        if os.path.exists(self.testdir):
            for dirpath, subdirs, filenames in os.walk(self.testdir, topdown=False):
                for name in filenames:
                    os.remove(os.path.join(dirpath, name))
                for name in subdirs:
                    os.rmdir(os.path.join(dirpath, name))
        if os.path.exists(self.testdir):
            os.rmdir(self.testdir)
        assert not os.path.exists(self.testdir)

    def run_command(self, *argv, **kwargs) -> None:
        """Runs the command via the main function, passing self.defaultoptions and keyword arguments as --long options and argv arguments straight."""
        os.chdir(self.testdir)
        argv = list(argv)
        kwoptions = getattr(self, "defaultoptions", {}).copy()
        kwoptions.update(kwargs)
        for key, value in kwoptions.items():
            key = key.replace("_", "-")
            if value is True:
                argv.append(f"--{key}")
            else:
                argv.append(f"--{key}={value}")
        try:
            self.convertmodule.main(argv)
        finally:
            os.chdir(self.rundir)

    def get_testfilename(self, filename):
        """Gets the path to the test file."""
        return os.path.join(self.testdir, filename)

    def open_testfile(self, filename, mode="rb"):
        """Opens the given filename in the testdirectory in the given mode."""
        filename = self.get_testfilename(filename)
        if not mode.startswith("r"):
            subdir = os.path.dirname(filename)
            currentpath = ""
            if not os.path.isdir(subdir):
                for part in subdir.split(os.sep):
                    currentpath = os.path.join(currentpath, part)
                    if not os.path.isdir(currentpath):
                        os.mkdir(currentpath)
        return open(filename, mode)

    def create_testfile(self, filename, contents) -> None:
        """Creates the given file in the testdirectory with the given contents."""
        if isinstance(contents, str):
            contents = contents.encode("utf-8")
        testfile = self.open_testfile(filename, "wb")
        testfile.write(contents)
        testfile.close()

    def read_testfile(self, filename):
        """Reads the given file in the testdirectory and returns the contents."""
        with open(self.get_testfilename(filename), "rb") as testfile:
            return testfile.read()

    @staticmethod
    def help_check(options, option, last=False):
        """Check that a help string occurs and remove it."""
        assert option in options
        newoptions = []
        for line in options.splitlines():
            if option in line or not line.lstrip().startswith("-"):
                continue
            newoptions.append(line)
        if last:
            assert newoptions == []
        return "\n".join(newoptions)

    def test_help(self, capsys) -> None:
        """Tests getting help (returning the help_string so further tests can be done)."""
        with pytest.raises(SystemExit):
            self.run_command(help=True)
        help_string, _err = capsys.readouterr()
        # normalize newlines
        help_lines = help_string.splitlines()
        print(help_lines)

        # summary documentation
        convertsummary = self.convertmodule.__doc__.split("\n", maxsplit=1)[0]  # ty:ignore[unresolved-attribute]
        # the convertsummary might be wrapped. this will probably unwrap it
        assert convertsummary in " ".join(help_lines)

        # usage line
        usageline = help_lines[0]
        assert usageline.startswith("Usage: ")
        assert "[--version] [-h|--help]" in usageline
        for line in help_lines:
            print(line)

        # extract options
        options = [
            line.lstrip()
            for line in help_lines[help_lines.index("Options:") + 1 :]
            if OPTION_RE.match(line)
        ]

        # Verify all expected options are present
        base_options = [
            "--progress=PROGRESS",
            "--version",
            "-h, --help",
            "--manpage",
            "--errorlevel=ERRORLEVEL",
            "-i INPUT, --input=INPUT",
            "-x EXCLUDE, --exclude=EXCLUDE",
            "-o OUTPUT, --output=OUTPUT",
            "-S, --timestamp",
        ]
        for expected in chain(base_options, self.expected_options):
            start = len(options)
            options = [option for option in options if not option.startswith(expected)]
            assert start - 1 == len(options), f"{expected} not found in {options}"

        # We should parse all the options
        assert options == []


class TestArchiveConvertOptionParser:
    def test_recursiveprocess_closes_input_archive(self, tmp_path) -> None:
        processed = []

        class FakeArchive:
            instances = []

            def __init__(self, filename, **kwargs) -> None:
                self.filename = filename
                self.closed = False
                self.__class__.instances.append(self)

            def __iter__(self):
                yield os.path.join("nested", "file.txt")

            def openinputfile(self, subfile):
                inputfile = BytesIO(b"content")
                inputfile.filename = subfile  # ty:ignore[unresolved-attribute]
                return inputfile

            def close(self) -> None:
                self.closed = True

        def processor(inputfile, outputfile, templatefile):
            processed.append(True)
            outputfile.write(inputfile.read())
            return True

        parser = convert.ArchiveConvertOptionParser(
            {"txt": ("po", processor)},
            archiveformats={(None, "input"): FakeArchive},
        )
        inputarchive = tmp_path / "input.oo"
        outputdir = tmp_path / "output"
        inputarchive.write_text("unused", encoding="utf-8")
        options = SimpleNamespace(
            input=str(inputarchive),
            output=str(outputdir),
            template=None,
            progress="none",
            errorlevel="message",
            timestamp=False,
            exclude=["CVS", ".svn", ".git"],
        )

        parser.recursiveprocess(options)

        assert processed == [True]
        assert FakeArchive.instances[0].closed is True
        assert (outputdir / "nested" / "file.po").read_bytes() == b"content"


class TestShouldOutputStore:
    @staticmethod
    def get_store(po_source):
        return po.pofile(BytesIO(po_source.encode()))

    def test_header_only_store_below_threshold(self) -> None:
        store = self.get_store(
            """msgid ""
msgstr ""
"Project-Id-Version: test\\n"
"""
        )

        assert convert.should_output_store(store, 70) is False

    def test_zero_word_store_below_threshold(self) -> None:
        store = self.get_store(
            """#: key
msgid "<br/>"
msgstr "<br/>"
"""
        )

        assert convert.should_output_store(store, 70) is False
