"""Tests for fileutils."""

import io
import os
import unittest
from unittest.mock import MagicMock
from unittest.mock import patch

from gam import fileutils


class FileutilsTest(unittest.TestCase):

    def setUp(self):
        self.fake_path = '/some/path/to/file'
        super().setUp()

    @patch.object(fileutils.sys, 'stdin')
    def test_open_file_stdin(self, mock_stdin):
        mock_stdin.read.return_value = 'some stdin content'
        f = fileutils.open_file('-', mode='r')
        self.assertIsInstance(f, fileutils.io.StringIO)
        self.assertEqual(f.getvalue(), mock_stdin.read.return_value)

    def test_open_file_stdout(self):
        f = fileutils.open_file('-', mode='w')
        self.assertEqual(fileutils.sys.stdout, f)

    @patch.object(fileutils, 'open', new_callable=unittest.mock.mock_open)
    def test_open_file_opens_correct_path(self, mock_open):
        f = fileutils.open_file(self.fake_path)
        self.assertEqual(self.fake_path, mock_open.call_args[0][0])
        self.assertEqual(mock_open.return_value, f)

    @patch.object(fileutils, 'open', new_callable=unittest.mock.mock_open)
    def test_open_file_expands_user_file_path(self, mock_open):
        file_path = '~/some/path/containing/tilde/shortcut/to/home'
        fileutils.open_file(file_path)
        opened_path = mock_open.call_args[0][0]
        home_path = os.environ.get('HOME')
        self.assertIsNotNone(home_path)
        self.assertIn(home_path, opened_path)

    @patch.object(fileutils, 'open', new_callable=unittest.mock.mock_open)
    def test_open_file_opens_correct_mode(self, mock_open):
        fileutils.open_file(self.fake_path)
        self.assertEqual('r', mock_open.call_args[0][1])

    @patch.object(fileutils, 'open', new_callable=unittest.mock.mock_open)
    def test_open_file_encoding_for_binary(self, mock_open):
        fileutils.open_file(self.fake_path, mode='b')
        self.assertIsNone(mock_open.call_args[1]['encoding'])

    @patch.object(fileutils, 'open', new_callable=unittest.mock.mock_open)
    def test_open_file_default_system_encoding(self, mock_open):
        fileutils.open_file(self.fake_path)
        self.assertEqual(fileutils.GM_Globals[fileutils.GM_SYS_ENCODING],
                         mock_open.call_args[1]['encoding'])

    @patch.object(fileutils, 'open', new_callable=unittest.mock.mock_open)
    def test_open_file_utf8_encoding_specified(self, mock_open):
        fileutils.open_file(self.fake_path, encoding='UTF-8')
        self.assertEqual(fileutils.UTF8_SIG, mock_open.call_args[1]['encoding'])

    def test_open_file_strips_utf_bom_in_utf(self):
        bom_prefixed_data = '\ufefffoobar'
        fake_file = io.StringIO(bom_prefixed_data)
        mock_open = MagicMock(spec=open, return_value=fake_file)
        with patch.object(fileutils, 'open', mock_open):
            f = fileutils.open_file(self.fake_path, strip_utf_bom=True)
            self.assertEqual('foobar', f.read())

    def test_open_file_strips_utf_bom_in_non_utf(self):
        bom_prefixed_data = b'\xef\xbb\xbffoobar'.decode('iso-8859-1')

        # We need to trick the method under test into believing that a StringIO
        # instance is a file with an encoding. Since StringIO does not usually have,
        # an encoding, we'll mock it and add our own encoding, but send the other
        # methods in use (read and seek) back to the real StringIO object.
        real_stringio = io.StringIO(bom_prefixed_data)
        mock_file = MagicMock(spec=io.StringIO)
        mock_file.read.side_effect = real_stringio.read
        mock_file.seek.side_effect = real_stringio.seek
        mock_file.encoding = 'iso-8859-1'

        mock_open = MagicMock(spec=open, return_value=mock_file)
        with patch.object(fileutils, 'open', mock_open):
            f = fileutils.open_file(self.fake_path, strip_utf_bom=True)
            self.assertEqual('foobar', f.read())

    def test_open_file_strips_utf_bom_in_binary(self):
        bom_prefixed_data = '\ufefffoobar'.encode()
        fake_file = io.BytesIO(bom_prefixed_data)
        mock_open = MagicMock(spec=open, return_value=fake_file)
        with patch.object(fileutils, 'open', mock_open):
            f = fileutils.open_file(self.fake_path,
                                    mode='rb',
                                    strip_utf_bom=True)
            self.assertEqual(b'foobar', f.read())

    def test_open_file_strip_utf_bom_when_no_bom_in_data(self):
        no_bom_data = 'This data has no BOM'
        fake_file = io.StringIO(no_bom_data)
        mock_open = MagicMock(spec=open, return_value=fake_file)

        with patch.object(fileutils, 'open', mock_open):
            f = fileutils.open_file(self.fake_path, strip_utf_bom=True)
            # Since there was no opening BOM, we should be back at the beginning of
            # the file.
            self.assertEqual(fake_file.tell(), 0)
            self.assertEqual(f.read(), no_bom_data)

    @patch.object(fileutils, 'open', new_callable=unittest.mock.mock_open)
    def test_open_file_exits_on_io_error(self, mock_open):
        mock_open.side_effect = IOError('Fake IOError')
        with self.assertRaises(SystemExit) as context:
            fileutils.open_file(self.fake_path)
        self.assertEqual(context.exception.code, 6)

    def test_close_file_closes_file_successfully(self):
        mock_file = MagicMock()
        self.assertTrue(fileutils.close_file(mock_file))
        self.assertEqual(mock_file.close.call_count, 1)

    def test_close_file_with_error(self):
        mock_file = MagicMock()
        mock_file.close.side_effect = IOError()
        self.assertFalse(fileutils.close_file(mock_file))
        self.assertEqual(mock_file.close.call_count, 1)

    @patch.object(fileutils.sys, 'stdin')
    def test_read_file_from_stdin(self, mock_stdin):
        mock_stdin.read.return_value = 'some stdin content'
        self.assertEqual(fileutils.read_file('-'), mock_stdin.read.return_value)

    @patch.object(fileutils, '_open_file')
    def test_read_file_default_params(self, mock_open_file):
        fake_content = 'some fake content'
        mock_open_file.return_value.__enter__().read.return_value = fake_content
        self.assertEqual(fileutils.read_file(self.fake_path), fake_content)
        self.assertEqual(mock_open_file.call_args[0][0], self.fake_path)
        self.assertEqual(mock_open_file.call_args[0][1], 'r')
        self.assertIsNone(mock_open_file.call_args[1]['newline'])

    @patch.object(fileutils.display, 'print_warning')
    @patch.object(fileutils, '_open_file')
    def test_read_file_continues_on_errors_without_displaying(
        self, mock_open_file, mock_print_warning):
        mock_open_file.side_effect = IOError()
        contents = fileutils.read_file(self.fake_path,
                                       continue_on_error=True,
                                       display_errors=False)
        self.assertIsNone(contents)
        self.assertFalse(mock_print_warning.called)

    @patch.object(fileutils.display, 'print_warning')
    @patch.object(fileutils, '_open_file')
    def test_read_file_displays_errors(self, mock_open_file,
                                       mock_print_warning):
        mock_open_file.side_effect = IOError()
        fileutils.read_file(self.fake_path,
                            continue_on_error=True,
                            display_errors=True)
        self.assertTrue(mock_print_warning.called)

    @patch.object(fileutils, '_open_file')
    def test_read_file_exits_code_6_when_continue_on_error_is_false(
        self, mock_open_file):
        mock_open_file.side_effect = IOError()
        with self.assertRaises(SystemExit) as context:
            fileutils.read_file(self.fake_path, continue_on_error=False)
        self.assertEqual(context.exception.code, 6)

    @patch.object(fileutils, '_open_file')
    def test_read_file_exits_code_2_on_lookuperror(self, mock_open_file):
        mock_open_file.return_value.__enter__().read.side_effect = LookupError()
        with self.assertRaises(SystemExit) as context:
            fileutils.read_file(self.fake_path)
        self.assertEqual(context.exception.code, 2)

    @patch.object(fileutils, '_open_file')
    def test_read_file_exits_code_2_on_unicodeerror(self, mock_open_file):
        mock_open_file.return_value.__enter__().read.side_effect = UnicodeError(
        )
        with self.assertRaises(SystemExit) as context:
            fileutils.read_file(self.fake_path)
        self.assertEqual(context.exception.code, 2)

    @patch.object(fileutils, '_open_file')
    def test_read_file_exits_code_2_on_unicodedecodeerror(self, mock_open_file):
        fake_decode_error = UnicodeDecodeError('fake-encoding', b'fakebytes', 0,
                                               1, 'testing only')
        mock_open_file.return_value.__enter__(
        ).read.side_effect = fake_decode_error
        with self.assertRaises(SystemExit) as context:
            fileutils.read_file(self.fake_path)
        self.assertEqual(context.exception.code, 2)

    @patch.object(fileutils, '_open_file')
    def test_write_file_writes_data_to_file(self, mock_open_file):
        fake_data = 'some fake data'
        fileutils.write_file(self.fake_path, fake_data)
        self.assertEqual(mock_open_file.call_args[0][0], self.fake_path)
        self.assertEqual(mock_open_file.call_args[0][1], 'w')

        opened_file = mock_open_file.return_value.__enter__()
        self.assertTrue(opened_file.write.called)
        self.assertEqual(opened_file.write.call_args[0][0], fake_data)

    @patch.object(fileutils.display, 'print_error')
    @patch.object(fileutils, '_open_file')
    def test_write_file_continues_on_errors_without_displaying(
        self, mock_open_file, mock_print_error):
        mock_open_file.side_effect = IOError()
        status = fileutils.write_file(self.fake_path,
                                      'foo data',
                                      continue_on_error=True,
                                      display_errors=False)
        self.assertFalse(status)
        self.assertFalse(mock_print_error.called)

    @patch.object(fileutils.display, 'print_error')
    @patch.object(fileutils, '_open_file')
    def test_write_file_displays_errors(self, mock_open_file, mock_print_error):
        mock_open_file.side_effect = IOError()
        fileutils.write_file(self.fake_path,
                             'foo data',
                             continue_on_error=True,
                             display_errors=True)
        self.assertTrue(mock_print_error.called)

    @patch.object(fileutils, '_open_file')
    def test_write_file_exits_code_6_when_continue_on_error_is_false(
        self, mock_open_file):
        mock_open_file.side_effect = IOError()
        with self.assertRaises(SystemExit) as context:
            fileutils.write_file(self.fake_path,
                                 'foo data',
                                 continue_on_error=False)
        self.assertEqual(context.exception.code, 6)


if __name__ == '__main__':
    unittest.main()
