import errno
from functools import partial
from unittest import mock

import pytest

from snakeoil.cli.input import NoChoice, userquery
from snakeoil.test.argparse_helpers import FakeStreamFormatter


@pytest.fixture
def mocked_input():
    with mock.patch("builtins.input") as mocked_input:
        yield mocked_input


class TestUserQuery:
    @pytest.fixture(autouse=True)
    def __setup(self):
        self.out = FakeStreamFormatter()
        self.err = FakeStreamFormatter()
        self.query = partial(userquery, out=self.out, err=self.err)

    def test_default_answer(self, mocked_input):
        mocked_input.return_value = ""
        assert self.query("foo") is True

    def test_tuple_prompt(self, mocked_input):
        mocked_input.return_value = ""
        prompt = "perhaps a tuple"
        assert self.query(tuple(prompt.split())) is True
        output = "".join(prompt.split())
        assert (
            self.out.get_text_stream().strip().split("\n")[0][: len(output)] == output
        )

    def test_no_default_answer(self, mocked_input):
        responses = {
            "a": ("z", "Yes"),
            "b": ("y", "No"),
        }
        # no default answer returns None for empty input
        mocked_input.return_value = ""
        assert self.query("foo", responses=responses) is None
        mocked_input.return_value = "a"
        assert self.query("foo", responses=responses) == "z"
        mocked_input.return_value = "b"
        assert self.query("foo", responses=responses) == "y"

    def test_ambiguous_input(self, mocked_input):
        responses = {
            "a": ("z", "Yes"),
            "A": ("y", "No"),
        }
        mocked_input.return_value = "a"
        with pytest.raises(NoChoice):
            self.query("foo", responses=responses)
        error_output = self.err.get_text_stream().strip().split("\n")[1]
        expected = "Response %r is ambiguous (%s)" % (
            mocked_input.return_value,
            ", ".join(sorted(responses.keys())),
        )
        assert error_output == expected

    def test_default_correct_input(self, mocked_input):
        for input, output in (
            ("no", False),
            ("No", False),
            ("yes", True),
            ("Yes", True),
        ):
            mocked_input.return_value = input
            assert self.query("foo") == output

    def test_default_answer_no_matches(self, mocked_input):
        mocked_input.return_value = ""
        with pytest.raises(ValueError):
            self.query("foo", default_answer="foo")
        assert self.out.stream == []

    def test_custom_default_answer(self, mocked_input):
        mocked_input.return_value = ""
        assert self.query("foo", default_answer=False) is False

    def test_eof_nochoice(self, mocked_input):
        # user hits ctrl-d
        mocked_input.side_effect = EOFError
        with pytest.raises(NoChoice):
            self.query("foo")
        output = self.out.get_text_stream().strip().split("\n")[1]
        expected = "Not answerable: EOF on STDIN"
        assert output == expected

    def test_stdin_closed_nochoice(self, mocked_input):
        mocked_input.side_effect = IOError(errno.EBADF, "")
        with pytest.raises(NoChoice):
            self.query("foo")
        output = self.out.get_text_stream().strip().split("\n")[1]
        expected = "Not answerable: STDIN is either closed, or not readable"
        assert output == expected

    def test_unhandled_ioerror(self, mocked_input):
        mocked_input.side_effect = IOError(errno.ENODEV, "")
        with pytest.raises(IOError):
            self.query("foo")

    def test_bad_choice_limit(self, mocked_input):
        # user hits enters a bad choice 3 times in a row
        mocked_input.return_value = "bad"
        with pytest.raises(NoChoice):
            self.query("foo")
        assert mocked_input.call_count == 3
        output = self.err.get_text_stream().strip().split("\n")[1]
        expected = "Sorry, response %r not understood." % (mocked_input.return_value,)
        assert output == expected

    def test_custom_choice_limit(self, mocked_input):
        # user hits enters a bad choice 5 times in a row
        mocked_input.return_value = "haha"
        with pytest.raises(NoChoice):
            self.query("foo", limit=5)
        assert mocked_input.call_count == 5
        output = self.err.get_text_stream().strip().split("\n")[1]
        expected = "Sorry, response %r not understood." % (mocked_input.return_value,)
        assert output == expected
