#   Licensed under the Apache License, Version 2.0 (the "License"); you may
#   not use this file except in compliance with the License. You may obtain
#   a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#   License for the specific language governing permissions and limitations
#   under the License.

import codecs
import subprocess
import sys
import unittest
from unittest import mock

import fixtures  # type: ignore

from typing import Optional, List, Dict

from autopage.tests import sinks

import autopage
from autopage import command


_PagerConfig = command.PagerConfig


class PagedStreamTest(fixtures.TestWithFixtures):
    def setUp(self) -> None:
        out = sinks.TTYFixture()
        self.useFixture(out)
        self.stream = out.stream
        self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.stream))
        popen = fixtures.MockPatch('subprocess.Popen')
        self.useFixture(popen)
        self.popen = popen.mock

    def test_defaults(self) -> None:
        class TestCommand(command.PagerCommand):
            def command(self) -> List[str]:
                return []

            def environment_variables(
                    self,
                    config: _PagerConfig) -> Optional[Dict[str, str]]:
                return None

        tc = TestCommand()
        ap = autopage.AutoPager(pager_command=tc, line_buffering=False)
        with mock.patch.object(ap, '_pager_env') as get_env, \
                mock.patch.object(tc, 'command') as cmd:
            stream = ap._paged_stream()
            self.popen.assert_called_once_with(
                cmd.return_value,
                env=get_env.return_value,
                bufsize=-1,
                universal_newlines=True,
                encoding='utf-8',
                errors='strict',
                stdin=subprocess.PIPE,
                stdout=None)
            self.assertIs(stream, self.popen.return_value.stdin)

    def test_defaults_cmd_as_class(self) -> None:
        class TestCommand(command.PagerCommand):
            def command(self) -> List[str]:
                return []

            def environment_variables(
                    self,
                    config: _PagerConfig) -> Optional[Dict[str, str]]:
                return None

        with mock.patch.object(TestCommand, 'command') as cmd:
            ap = autopage.AutoPager(pager_command=TestCommand,
                                    line_buffering=False)
            with mock.patch.object(ap, '_pager_env') as get_env:
                stream = ap._paged_stream()
                self.popen.assert_called_once_with(
                    cmd.return_value,
                    env=get_env.return_value,
                    bufsize=-1,
                    universal_newlines=True,
                    encoding='utf-8',
                    errors='strict',
                    stdin=subprocess.PIPE,
                    stdout=None)
                self.assertIs(stream, self.popen.return_value.stdin)

    def test_defaults_cmd_as_string(self) -> None:
        ap = autopage.AutoPager(pager_command='foo bar',
                                line_buffering=False)
        with mock.patch.object(ap, '_pager_env') as get_env:
            stream = ap._paged_stream()
            self.popen.assert_called_once_with(
                ['foo', 'bar'],
                env=get_env.return_value,
                bufsize=-1,
                universal_newlines=True,
                encoding='utf-8',
                errors='strict',
                stdin=subprocess.PIPE,
                stdout=None)
            self.assertIs(stream, self.popen.return_value.stdin)

    def test_defaults_cmd_as_int(self) -> None:
        self.assertRaises(TypeError, autopage.AutoPager,
                          pager_command=42)

    def test_line_buffering(self) -> None:
        ap = autopage.AutoPager(line_buffering=True)
        stream = ap._paged_stream()
        self.popen.assert_called_once_with(
            mock.ANY,
            env=mock.ANY,
            bufsize=1,
            universal_newlines=True,
            encoding=mock.ANY,
            errors=mock.ANY,
            stdin=subprocess.PIPE,
            stdout=None)
        self.assertIs(stream, self.popen.return_value.stdin)

    def test_errors(self) -> None:
        ap = autopage.AutoPager(errors=autopage.ErrorStrategy.NAME_REPLACE)
        stream = ap._paged_stream()
        self.popen.assert_called_once_with(
            mock.ANY,
            env=mock.ANY,
            bufsize=mock.ANY,
            universal_newlines=mock.ANY,
            encoding=mock.ANY,
            errors='namereplace',
            stdin=subprocess.PIPE,
            stdout=None)
        self.assertIs(stream, self.popen.return_value.stdin)

    def test_explicit_stdout_stream(self) -> None:
        ap = autopage.AutoPager(self.stream)
        stream = ap._paged_stream()
        self.popen.assert_called_once_with(
            mock.ANY,
            env=mock.ANY,
            bufsize=mock.ANY,
            universal_newlines=mock.ANY,
            encoding=mock.ANY,
            errors=mock.ANY,
            stdin=subprocess.PIPE,
            stdout=None)
        self.assertIs(stream, self.popen.return_value.stdin)

    def test_explicit_stream(self) -> None:
        with sinks.TTYFixture() as tty:
            ap = autopage.AutoPager(tty.stream)
            stream = ap._paged_stream()
            self.popen.assert_called_once_with(
                mock.ANY,
                env=mock.ANY,
                bufsize=mock.ANY,
                universal_newlines=mock.ANY,
                encoding=mock.ANY,
                errors=mock.ANY,
                stdin=subprocess.PIPE,
                stdout=tty.stream)
            self.assertIs(stream, self.popen.return_value.stdin)


class ToTerminalTest(unittest.TestCase):
    def test_pty(self) -> None:
        with sinks.TTYFixture() as out:
            ap = autopage.AutoPager(out.stream)
            self.assertTrue(ap.to_terminal())

    def test_stringio(self) -> None:
        with sinks.BufferFixture() as out:
            ap = autopage.AutoPager(out.stream)
            self.assertFalse(ap.to_terminal())

    def test_file(self) -> None:
        with sinks.TempFixture() as out:
            ap = autopage.AutoPager(out.stream)
            self.assertFalse(ap.to_terminal())

    def test_default_pty(self) -> None:
        with sinks.TTYFixture() as out:
            with fixtures.MonkeyPatch('sys.stdout', out.stream):
                ap = autopage.AutoPager()
            self.assertTrue(ap.to_terminal())

    def test_default_file(self) -> None:
        with sinks.TempFixture() as out:
            with fixtures.MonkeyPatch('sys.stdout', out.stream):
                ap = autopage.AutoPager()
            self.assertFalse(ap.to_terminal())

    def test_launch_pager(self) -> None:
        ap = autopage.AutoPager()
        with mock.patch.object(ap, 'to_terminal', return_value=True), \
                mock.patch.object(ap, '_paged_stream') as page, \
                mock.patch.object(ap, '_reconfigure_output_stream') as reconf:
            with ap as stream:
                page.assert_called_once()
                self.assertIs(page.return_value, stream)
                reconf.assert_not_called()

    def test_launch_pager_fail(self) -> None:
        outstream = mock.Mock()
        ap = autopage.AutoPager(outstream)
        with mock.patch.object(ap, 'to_terminal', return_value=True), \
                mock.patch.object(ap, '_paged_stream',
                                  side_effect=OSError) as page, \
                mock.patch.object(ap, '_reconfigure_output_stream') as reconf:
            with ap as stream:
                page.assert_called_once()
                reconf.assert_called_once()
                self.assertIs(outstream, stream)

    def test_no_pager(self) -> None:
        outstream = mock.Mock()
        ap = autopage.AutoPager(outstream)
        with mock.patch.object(ap, 'to_terminal', return_value=False), \
                mock.patch.object(ap, '_paged_stream') as page, \
                mock.patch.object(ap, '_reconfigure_output_stream') as reconf:
            with ap as stream:
                page.assert_not_called()
                self.assertIs(outstream, stream)
                reconf.assert_called_once()

    def test_pager_cat(self) -> None:
        outstream = mock.Mock()
        cat = command.CustomPager('cat')
        ap = autopage.AutoPager(outstream, pager_command=cat)
        with mock.patch.object(ap, 'to_terminal', return_value=True), \
                mock.patch.object(ap, '_paged_stream') as page, \
                mock.patch.object(ap, '_reconfigure_output_stream') as reconf:
            with ap as stream:
                page.assert_not_called()
                self.assertIs(outstream, stream)
                reconf.assert_called_once()


class ExitCodeTest(fixtures.TestWithFixtures):
    def setUp(self) -> None:
        out = sinks.BufferFixture()
        self.useFixture(out)
        self.ap = autopage.AutoPager(out.stream)

    def test_success(self) -> None:
        with self.ap:
            pass
        self.assertEqual(0, self.ap.exit_code())

    def test_pager_broken_pipe_flush(self) -> None:
        flush = mock.MagicMock(side_effect=BrokenPipeError)
        with sinks.TTYFixture() as out:
            ap = autopage.AutoPager(out.stream)
            with fixtures.MockPatch('subprocess.Popen') as popen:
                with sinks.BufferFixture() as pager_in:
                    popen.mock.return_value.stdin = pager_in.stream
                    with ap as stream:
                        stream.write('foo')
                        stream.close = flush  # type: ignore
            self.assertEqual(141, ap.exit_code())

    def test_no_pager_broken_pipe_flush(self) -> None:
        flush = mock.MagicMock(side_effect=BrokenPipeError)
        with self.ap as stream:
            stream.write('foo')
            stream.flush = flush  # type: ignore
        self.assertEqual(141, self.ap.exit_code())

    def test_broken_pipe(self) -> None:
        with self.ap:
            raise BrokenPipeError
        self.assertEqual(141, self.ap.exit_code())

    def test_exception(self) -> None:
        class MyException(Exception):
            pass

        def run() -> None:
            with self.ap:
                raise MyException

        self.assertRaises(MyException, run)
        self.assertEqual(1, self.ap.exit_code())

    def test_exception_on_error_default(self) -> None:
        class MyException(Exception):
            pass

        def run() -> None:
            with self.ap:
                raise MyException

        self.assertRaises(MyException, run)
        self.assertEqual(2, self.ap.exit_code(on_error_default=2))

    def test_base_exception(self) -> None:
        class MyBaseException(BaseException):
            pass

        def run() -> None:
            with self.ap:
                raise MyBaseException

        self.assertRaises(MyBaseException, run)
        self.assertEqual(1, self.ap.exit_code())

    def test_interrupt(self) -> None:
        def run() -> None:
            with self.ap:
                raise KeyboardInterrupt

        self.assertRaises(KeyboardInterrupt, run)
        self.assertEqual(130, self.ap.exit_code())

    def test_system_exit(self) -> None:
        def run() -> None:
            with self.ap:
                raise SystemExit(42)

        self.assertRaises(SystemExit, run)
        self.assertEqual(42, self.ap.exit_code())

    def test_system_exit_on_error_default(self) -> None:
        def run() -> None:
            with self.ap:
                raise SystemExit(42)

        self.assertRaises(SystemExit, run)
        self.assertEqual(42, self.ap.exit_code(on_error_default=3))

    def test_system_exit_string(self) -> None:
        def run() -> None:
            with self.ap:
                raise SystemExit("nope")

        self.assertRaises(SystemExit, run)
        self.assertEqual(1, self.ap.exit_code())

    def test_system_exit_string_on_error_default(self) -> None:
        def run() -> None:
            with self.ap:
                raise SystemExit("nope")

        self.assertRaises(SystemExit, run)
        self.assertEqual(3, self.ap.exit_code(on_error_default=3))


class CleanupTest(unittest.TestCase):
    def test_no_pager_stream_not_closed(self) -> None:
        flush = mock.MagicMock()
        with sinks.BufferFixture() as out:
            with autopage.AutoPager(out.stream) as stream:
                stream.flush = flush  # type: ignore
                stream.write('foo')
            self.assertFalse(out.stream.closed)
        flush.assert_called_once()

    def test_no_pager_broken_pipe(self) -> None:
        flush = mock.MagicMock(side_effect=BrokenPipeError)
        with sinks.BufferFixture() as out:
            with autopage.AutoPager(out.stream) as stream:
                stream.flush = flush  # type: ignore
                stream.write('foo')
            self.assertTrue(out.stream.closed)
        flush.assert_called_once()

    def test_no_pager_broken_pipe_flush(self) -> None:
        flush = mock.MagicMock(side_effect=BrokenPipeError)
        with sinks.BufferFixture() as out:
            with autopage.AutoPager(out.stream) as stream:
                stream.write('foo')
                stream.flush = flush  # type: ignore
            self.assertTrue(out.stream.closed)
        flush.assert_called_once()

    def test_no_pager_stream_closed(self) -> None:
        flush = mock.MagicMock(side_effect=ValueError)
        with sinks.BufferFixture() as out:
            with autopage.AutoPager(out.stream) as stream:
                stream.write('foo')
                stream.close()
                # Calling flush() on a closed stream raises an exception for
                # real streams (but not for StringIO).
                stream.flush = flush  # type: ignore
            self.assertTrue(out.stream.closed)

    def test_pager_stream_not_closed(self) -> None:
        with sinks.TTYFixture() as out:
            ap = autopage.AutoPager(out.stream)
            with fixtures.MockPatch('subprocess.Popen') as popen:
                with sinks.BufferFixture() as pager_in:
                    popen.mock.return_value.stdin = pager_in.stream
                    with ap as stream:
                        self.assertIs(pager_in.stream, stream)
                    self.assertTrue(pager_in.stream.closed)

    def test_pager_stream_not_closed_interrupt(self) -> None:
        with sinks.TTYFixture() as out:
            ap = autopage.AutoPager(out.stream)
            with fixtures.MockPatch('subprocess.Popen') as popen:
                with sinks.BufferFixture() as pager_in:
                    popen.mock.return_value.stdin = pager_in.stream

                    def run() -> None:
                        with ap as stream:
                            self.assertIs(pager_in.stream, stream)
                            raise KeyboardInterrupt

                    self.assertRaises(KeyboardInterrupt, run)
                    self.assertTrue(pager_in.stream.closed)

    def test_pager_broken_pipe(self) -> None:
        flush = mock.MagicMock(side_effect=BrokenPipeError)
        with sinks.TTYFixture() as out:
            ap = autopage.AutoPager(out.stream)
            with fixtures.MockPatch('subprocess.Popen') as popen:
                with sinks.BufferFixture() as pager_in:
                    popen.mock.return_value.stdin = pager_in.stream
                    pager_in.stream.flush = flush
                    with ap as stream:
                        self.assertIs(pager_in.stream, stream)
                    self.assertTrue(pager_in.stream.closed)
                    popen.mock.return_value.wait.assert_called_once()

    def test_pager_stream_closed(self) -> None:
        with sinks.TTYFixture() as out:
            ap = autopage.AutoPager(out.stream)
            with fixtures.MockPatch('subprocess.Popen') as popen:
                with sinks.BufferFixture() as pager_in:
                    popen.mock.return_value.stdin = pager_in.stream
                    with ap as stream:
                        self.assertIs(pager_in.stream, stream)
                        stream.close()
                    popen.mock.return_value.wait.assert_called_once()


class StreamConfigureTest(fixtures.TestWithFixtures):
    def setUp(self) -> None:
        out = sinks.TempFixture()
        self.useFixture(out)
        self.stream = out.stream
        self.default_lb = self.stream.line_buffering
        self.default_errors = self.stream.errors
        self.encoding = codecs.lookup(self.stream.encoding).name

    def test_line_buffering_on(self) -> None:
        ap = autopage.AutoPager(self.stream, line_buffering=True)
        ap._reconfigure_output_stream()
        self.addCleanup(ap._out.close)
        self.assertTrue(ap._out.line_buffering)
        self.assertEqual(self.default_errors, ap._out.errors)
        self.assertEqual(self.encoding, ap._encoding())
        self.assertIs(True, ap._line_buffering())
        self.assertEqual(self.default_errors, ap._errors())

    def test_line_buffering_off(self) -> None:
        ap = autopage.AutoPager(self.stream, line_buffering=False)
        ap._reconfigure_output_stream()
        self.addCleanup(ap._out.close)
        self.assertFalse(ap._out.line_buffering)
        self.assertEqual(self.default_errors, ap._out.errors)
        self.assertEqual(self.encoding, ap._encoding())
        self.assertIs(False, ap._line_buffering())
        self.assertEqual(self.default_errors, ap._errors())

    def test_stdout_line_buffering_on(self) -> None:
        with fixtures.MonkeyPatch('sys.stdout', self.stream):
            ap = autopage.AutoPager(line_buffering=True)
            ap._reconfigure_output_stream()
            self.addCleanup(ap._out.close)
            self.assertTrue(sys.stdout.line_buffering)
            self.assertEqual(self.default_errors, sys.stdout.errors)
            self.assertEqual(self.encoding,
                             codecs.lookup(sys.stdout.encoding).name)

    def test_errors(self) -> None:
        ap = autopage.AutoPager(self.stream,
                                errors=autopage.ErrorStrategy.NAME_REPLACE)
        ap._reconfigure_output_stream()
        self.addCleanup(ap._out.close)
        self.assertEqual(self.default_lb, ap._out.line_buffering)
        self.assertEqual('namereplace', ap._out.errors)
        self.assertNotEqual(self.default_errors, ap._out.errors)
        self.assertEqual(self.encoding, ap._encoding())
        self.assertEqual('namereplace', ap._errors())
        self.assertEqual(self.default_lb, ap._line_buffering())

    def test_errors_string(self) -> None:
        ap = autopage.AutoPager(self.stream,
                                errors='namereplace')  # type: ignore
        ap._reconfigure_output_stream()
        self.addCleanup(ap._out.close)
        self.assertEqual(self.default_lb, ap._out.line_buffering)
        self.assertEqual('namereplace', ap._out.errors)
        self.assertNotEqual(self.default_errors, ap._out.errors)
        self.assertEqual(self.encoding, ap._encoding())
        self.assertEqual('namereplace', ap._errors())
        self.assertEqual(self.default_lb, ap._line_buffering())

    def test_errors_bogus_string(self) -> None:
        self.assertRaises(ValueError,
                          autopage.AutoPager,
                          self.stream, errors='panic')

    def test_line_buffering_on_errors(self) -> None:
        ap = autopage.AutoPager(self.stream,
                                line_buffering=True,
                                errors=autopage.ErrorStrategy.NAME_REPLACE)
        ap._reconfigure_output_stream()
        self.addCleanup(ap._out.close)
        self.assertTrue(ap._out.line_buffering)
        self.assertEqual('namereplace', ap._out.errors)
        self.assertNotEqual(self.default_errors, ap._out.errors)
        self.assertEqual(self.encoding, ap._encoding())
        self.assertIs(True, ap._line_buffering())
        self.assertEqual('namereplace', ap._errors())
