from io import StringIO

import pytest

from snakeoil.bash import (
    BashParseError,
    read_bash,
    read_bash_dict,
    read_dict,
)


class TestBashCommentStripping:
    def test_read_bash(self):
        output = read_bash(
            StringIO("\n# hi I am a comment\nI am not \n asdf # inline comment\n")
        )
        assert list(output) == ["I am not", "asdf"]

        output = read_bash(StringIO("inline # comment "), allow_inline_comments=False)
        assert list(output) == ["inline # comment"]

    def test_read_bash_line_cont(self):
        output = read_bash(
            StringIO(
                "\n"
                "# hi I am a comment\\\n"
                "I am not \\\n"
                "a comment \n"
                " asdf # inline comment\\\n"
            ),
            allow_line_cont=True,
        )
        assert list(output) == ["I am not a comment", "asdf"]

        # continuation into inline comment
        output = read_bash(
            StringIO(
                "\n# hi I am a comment\nI am \\\nnot a \\\ncomment # inline comment\n"
            ),
            allow_line_cont=True,
        )
        assert list(output) == ["I am not a comment"]

        # ends with continuation
        output = read_bash(
            StringIO("\n# hi I am a comment\nI am \\\n\\\nnot a \\\ncomment\\\n\\\n"),
            allow_line_cont=True,
        )
        assert list(output) == ["I am not a comment"]

        # embedded comment prefix via continued lines
        output = read_bash(
            StringIO(
                "\\\n"
                "# comment\\\n"
                " not a comment\n"
                "\\\n"
                " # inner comment\n"
                "also not\\\n"
                "#\\\n"
                "a comment\n"
            ),
            allow_line_cont=True,
        )
        assert list(output) == ["not a comment", "also not#a comment"]

        # Line continuations have to end with \<newline> without any backslash
        # before the pattern.
        output = read_bash(StringIO("I am \\ \nnot a comment"), allow_line_cont=True)
        assert list(output) == ["I am \\", "not a comment"]
        output = read_bash(
            StringIO("\\\nI am \\\\\nnot a comment"), allow_line_cont=True
        )
        assert list(output) == ["I am \\\\", "not a comment"]


class TestReadDictConfig:
    def test_read_dict(self):
        bash_dict = read_dict(
            StringIO('\n# hi I am a comment\nfoo1=bar\nfoo2="bar"\nfoo3=\'bar"\n')
        )
        assert bash_dict == {
            "foo1": "bar",
            "foo2": "bar",
            "foo3": "'bar\"",
        }
        assert read_dict(["foo=bar"], source_isiter=True) == {"foo": "bar"}

        with pytest.raises(BashParseError):
            read_dict(["invalid"], source_isiter=True)

        bash_dict = read_dict(
            StringIO("foo bar\nfoo2  bar\nfoo3\tbar\n"), splitter=None
        )
        assert bash_dict == dict.fromkeys(("foo", "foo2", "foo3"), "bar")
        bash_dict = read_dict(["foo = blah", "foo2= blah ", "foo3=blah"], strip=True)
        assert bash_dict == dict.fromkeys(("foo", "foo2", "foo3"), "blah")


class TestReadBashDict:
    @pytest.fixture(autouse=True)
    def _setup(self, tmp_path):
        self.valid_file = tmp_path / "valid"
        self.valid_file.write_text(
            "# hi I am a comment\n"
            "foo1=bar\n"
            "foo2='bar'\n"
            'foo3="bar"\n'
            "foo4=-/:j4\n"
            "foo5=\n"
            'export foo6="bar"\n'
        )
        self.sourcing_file = tmp_path / "sourcing"
        self.sourcing_file.write_text(f'source "{self.valid_file}"\n')
        self.sourcing_file2 = tmp_path / "sourcing2"
        self.sourcing_file2.write_text(f'source "{self.valid_file}"\n')
        self.advanced_file = tmp_path / "advanced"
        self.advanced_file.write_text("one1=1\none_=$one1\ntwo1=2\ntwo_=${two1}\n")
        self.env_file = tmp_path / "env"
        self.env_file.write_text("imported=${external}\n")
        self.escaped_file = tmp_path / "escaped"
        self.escaped_file.write_text(
            'end=bye\nquoteddollar="\\${dollar}"\nquotedexpansion="\\${${end}}"\n'
        )
        self.unclosed_file = tmp_path / "unclosed"
        self.unclosed_file.write_text('foo="bar')

    def invoke_and_close(self, handle, *args, **kwds):
        try:
            return read_bash_dict(handle, *args, **kwds)
        finally:
            if hasattr(handle, "close"):
                handle.close()

    def test_read_bash_dict(self):
        # TODO this is not even close to complete
        bash_dict = self.invoke_and_close(str(self.valid_file))
        d = {
            "foo1": "bar",
            "foo2": "bar",
            "foo3": "bar",
            "foo4": "-/:j4",
            "foo5": "",
            "foo6": "bar",
        }
        assert bash_dict == d

        with pytest.raises(BashParseError):
            self.invoke_and_close(StringIO("a=b\ny='"))

    def test_var_read(self):
        assert self.invoke_and_close(StringIO("x=y@a\n")) == {"x": "y@a"}
        assert self.invoke_and_close(StringIO("x=y~a\n")) == {"x": "y~a"}
        assert self.invoke_and_close(StringIO("x=y^a\n")) == {"x": "y^a"}
        assert self.invoke_and_close(StringIO('x="\nasdf\nfdsa"')) == {
            "x": "\nasdf\nfdsa"
        }

    def test_empty_assign(self):
        self.valid_file.write_text("foo=\ndar=blah\n")
        assert self.invoke_and_close(str(self.valid_file)) == {"foo": "", "dar": "blah"}
        self.valid_file.write_text("foo=\ndar=\n")
        assert self.invoke_and_close(str(self.valid_file)) == {"foo": "", "dar": ""}
        self.valid_file.write_text("foo=blah\ndar=\n")
        assert self.invoke_and_close(str(self.valid_file)) == {"foo": "blah", "dar": ""}

    def test_quoting(self):
        assert self.invoke_and_close(StringIO("x='y \\\na'")) == {"x": "y \\\na"}
        assert self.invoke_and_close(StringIO("x='y'a\n")) == {"x": "ya"}
        assert self.invoke_and_close(StringIO('x="y \\\nasdf"')) == {"x": "y asdf"}

    def test_eof_without_newline(self):
        assert self.invoke_and_close(StringIO("x=y")) == {"x": "y"}
        assert self.invoke_and_close(StringIO("x='y'a")) == {"x": "ya"}

    def test_sourcing(self):
        output = self.invoke_and_close(
            str(self.sourcing_file), sourcing_command="source"
        )
        expected = {
            "foo1": "bar",
            "foo2": "bar",
            "foo3": "bar",
            "foo4": "-/:j4",
            "foo5": "",
            "foo6": "bar",
        }
        assert output == expected
        output = self.invoke_and_close(
            str(self.sourcing_file2), sourcing_command="source"
        )
        expected = {
            "foo1": "bar",
            "foo2": "bar",
            "foo3": "bar",
            "foo4": "-/:j4",
            "foo5": "",
            "foo6": "bar",
        }
        assert output == expected

    def test_read_advanced(self):
        output = self.invoke_and_close(str(self.advanced_file))
        expected = {
            "one1": "1",
            "one_": "1",
            "two1": "2",
            "two_": "2",
        }
        assert output == expected

    def test_env(self):
        assert self.invoke_and_close(str(self.env_file)) == {"imported": ""}
        env = {"external": "imported foo"}
        env_backup = env.copy()
        assert self.invoke_and_close(str(self.env_file), env) == {
            "imported": "imported foo"
        }
        assert env_backup == env

    def test_escaping(self):
        output = self.invoke_and_close(str(self.escaped_file))
        expected = {
            "end": "bye",
            "quoteddollar": "${dollar}",
            "quotedexpansion": "${bye}",
        }
        assert output == expected

    def test_unclosed(self):
        with pytest.raises(BashParseError):
            self.invoke_and_close(str(self.unclosed_file))

    def test_wordchards(self):
        assert self.invoke_and_close(StringIO("x=-*")) == {"x": "-*"}
