# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
#
"""Tests for autosave.py"""

# Standard library imports
import ast
import os.path as osp

# Third party imports
import pytest

# Local imports
from spyder.plugins.editor.utils.autosave import (AutosaveForStack,
                                                  AutosaveForPlugin)


def test_autosave_component_set_interval(mocker):
    """Test that setting the interval does indeed change it and calls
    do_autosave if enabled."""
    mocker.patch.object(AutosaveForPlugin, 'do_autosave')
    addon = AutosaveForPlugin(None)
    addon.do_autosave.assert_not_called()
    addon.interval = 10000
    assert addon.interval == 10000
    addon.do_autosave.assert_not_called()
    addon.enabled = True
    addon.interval = 20000
    assert addon.do_autosave.called


@pytest.mark.parametrize('enabled', [False, True])
def test_autosave_component_timer_if_enabled(qtbot, mocker, enabled):
    """Test that AutosaveForPlugin calls do_autosave() on timer if enabled."""
    mocker.patch.object(AutosaveForPlugin, 'do_autosave')
    addon = AutosaveForPlugin(None)
    addon.do_autosave.assert_not_called()
    addon.interval = 100
    addon.enabled = enabled
    qtbot.wait(500)
    if enabled:
        assert addon.do_autosave.called
    else:
        addon.do_autosave.assert_not_called()


def test_get_files_to_recover_with_empty_autosave_dir(mocker, tmpdir):
    """Test get_files_to_recover() when autosave dir contains no files."""
    mocker.patch('spyder.plugins.editor.utils.autosave.get_conf_path',
                 return_value=str(tmpdir))
    addon = AutosaveForPlugin(None)

    result = addon.get_files_to_recover()

    assert result == ([], [])


@pytest.mark.parametrize('running,empty',
                         [(True, False), (False, False), (False, True)])
def test_get_files_to_recover_with_one_pid_file(mocker, tmpdir,
                                                running, empty):
    """Test get_files_to_recover() if autosave dir contains one pid file with
    one autosave file. If running is True, then pretend that the pid file
    belongs to a running Spyder instance. If empty is True, then the pid file
    is empty (regression test for spyder-ide/spyder#11375)."""
    mocker.patch('spyder.plugins.editor.utils.autosave.get_conf_path',
                 return_value=str(tmpdir))
    mock_is_spyder_process = mocker.patch(
            'spyder.plugins.editor.utils.autosave.is_spyder_process',
            return_value=running)
    pidfile = tmpdir.join('pid42.txt')
    autosavefile = tmpdir.join('foo.py')
    if empty:
        pidfile.write('')
    else:
        pidfile.write('{"original": ' + repr(str(autosavefile)) + '}')
    autosavefile.write('bar = 1')
    addon = AutosaveForPlugin(None)

    result = addon.get_files_to_recover()

    if empty:  # pid file corrupted so original file name not recorded
        expected_files = [(None, str(autosavefile))]
    elif running:  # autosave file belongs to running instance
        expected_files = []
    else:
        expected_files = [('original', str(autosavefile))]
    expected = (expected_files, [str(pidfile)])
    assert result == expected
    mock_is_spyder_process.assert_called_with(42)


def test_get_files_to_recover_with_non_pid_file(mocker, tmpdir):
    """Test get_files_to_recover() if autosave dir contains no pid file, but
    one Python file."""
    mocker.patch('spyder.plugins.editor.utils.autosave.get_conf_path',
                 return_value=str(tmpdir))
    pythonfile = tmpdir.join('foo.py')
    pythonfile.write('bar = 1')
    addon = AutosaveForPlugin(None)

    result = addon.get_files_to_recover()

    expected = ([(None, str(pythonfile))], [])
    assert result == expected


def test_get_files_to_recover_without_autosave_dir(mocker):
    """Test that get_files_to_recover() does not break if there is no autosave
    directory."""
    mocker.patch('spyder.plugins.editor.utils.autosave.get_conf_path',
                 return_value='non-existing-directory')
    addon = AutosaveForPlugin(None)

    result = addon.get_files_to_recover()

    assert result == ([], [])


@pytest.mark.parametrize('error_on_remove', [False, True])
def test_try_recover(mocker, tmpdir, error_on_remove):
    """Test that try_recover_from_autosave() displays a RecoveryDialog, that
    it stores the files that the user wants to open as reported by the dialog,
    and that it removes the pid file. If error_on_remove is set, then
    removing the pid file will raise an OSError; this should be ignored."""
    mock_RecoveryDialog = mocker.patch(
            'spyder.plugins.editor.utils.autosave.RecoveryDialog')
    mocker.patch('spyder.plugins.editor.utils.autosave.get_conf_path',
                 return_value=str(tmpdir))
    pidfile = tmpdir.join('pid42.txt')
    autosavefile = tmpdir.join('foo.py')
    pidfile.write('{"original": ' + repr(str(autosavefile)) + '}')
    autosavefile.write('bar = 1')
    addon = AutosaveForPlugin(None)
    if error_on_remove:
        mocker.patch('os.remove', side_effect=OSError)

    addon.try_recover_from_autosave()

    expected_mapping = [('original', str(autosavefile))]
    mock_RecoveryDialog.assert_called_with(expected_mapping, parent=None)
    expected_files_to_open = mock_RecoveryDialog().files_to_open[:]
    assert addon.recover_files_to_open == expected_files_to_open
    if not error_on_remove:
        assert not pidfile.check()


@pytest.mark.parametrize('in_mapping,on_disk',
                         [(False, False), (True, False), (False, True)])
def test_create_unique_autosave_filename(mocker, in_mapping, on_disk):
    """Test that AutosaveForStack.create_unique_autosave_filename() returns
    a file name in the autosave directory with the same base name as the
    original file name, unless that already exists in the autosave mapping
    or on disk."""
    def new_exists(path):
        if path == osp.join('autosave', 'ham.py'):
            return on_disk
        else:
            return False

    mocker.patch('os.path.exists', side_effect=new_exists)
    addon = AutosaveForStack(mocker.Mock())
    if in_mapping:
        addon.name_mapping = {osp.join('somedir', 'ham.py'):
                              osp.join('autosave', 'ham.py')}

    autosave_filename = addon.create_unique_autosave_filename(
        osp.join('orig', 'ham.py'), 'autosave')

    if in_mapping or on_disk:
        assert autosave_filename == osp.join('autosave', 'ham-1.py')
    else:
        assert autosave_filename == osp.join('autosave', 'ham.py')


@pytest.mark.parametrize('have_hash', [True, False])
def test_autosave(mocker, have_hash):
    """Test that AutosaveForStack.maybe_autosave writes the contents to the
    autosave file and updates the file_hashes."""
    mock_editor = mocker.Mock()
    mock_fileinfo = mocker.Mock(editor=mock_editor, filename='orig',
                                newly_created=False)
    mock_document = mocker.Mock()
    mock_fileinfo.editor.document.return_value = mock_document
    mock_stack = mocker.Mock(data=[mock_fileinfo])
    addon = AutosaveForStack(mock_stack)
    addon.name_mapping = {'orig': 'autosave'}
    addon.file_hashes = {'autosave': 2}
    if have_hash:
        addon.file_hashes['orig'] = 1
    mock_stack.compute_hash.return_value = 3

    addon.maybe_autosave(0)

    mock_stack._write_to_file.assert_called_with(mock_fileinfo, 'autosave')
    mock_stack.compute_hash.assert_called_with(mock_fileinfo)
    if have_hash:
        assert addon.file_hashes == {'orig': 1, 'autosave': 3}
    else:
        assert addon.file_hashes == {'autosave': 3}


@pytest.mark.parametrize('latin', [True, False])
def test_save_autosave_mapping_with_nonempty_mapping(mocker, tmpdir, latin):
    """Test that save_autosave_mapping() writes the current autosave mapping
    to the correct file if the mapping is not empty."""
    mocker.patch('os.getpid', return_value=42)
    mocker.patch('spyder.plugins.editor.utils.autosave.get_conf_path',
                 return_value=str(tmpdir))
    addon = AutosaveForStack(None)
    if latin:
        addon.name_mapping = {'orig': 'autosave'}
    else:
        addon.name_mapping = {'原件': 'autosave'}

    addon.save_autosave_mapping()

    pidfile = tmpdir.join('pid42.txt')
    assert ast.literal_eval(pidfile.read()) == addon.name_mapping


@pytest.mark.parametrize('pidfile_exists', [False, True])
def test_save_autosave_mapping_with_empty_mapping(mocker, tmpdir,
                                                  pidfile_exists):
    """Test that save_autosave_mapping() does not write the pidfile if the
    mapping is empty, and that is removes the pidfile if it exists."""
    mocker.patch('os.getpid', return_value=42)
    mocker.patch('spyder.plugins.editor.utils.autosave.get_conf_path',
                 return_value=str(tmpdir))
    addon = AutosaveForStack(None)
    addon.name_mapping = {}
    pidfile = tmpdir.join('pid42.txt')
    if pidfile_exists:
        pidfile.write('This is an ex-parrot!')

    addon.save_autosave_mapping()

    assert not pidfile.check()


@pytest.mark.parametrize('exception', [False, True])
def test_autosave_remove_autosave_file(mocker, exception):
    """Test that AutosaveForStack.remove_autosave_file removes the autosave
    file, that an error dialog is displayed if an exception is raised,
    and that the autosave file is removed from `name_mapping` and
    `file_hashes`."""
    mock_remove = mocker.patch('os.remove')
    if exception:
        mock_remove.side_effect = OSError()
    mock_dialog = mocker.patch(
        'spyder.plugins.editor.utils.autosave.AutosaveErrorDialog')
    mock_stack = mocker.Mock()
    fileinfo = mocker.Mock()
    fileinfo.filename = 'orig'
    addon = AutosaveForStack(mock_stack)
    addon.name_mapping = {'orig': 'autosave'}
    addon.file_hashes = {'autosave': 42}

    addon.remove_autosave_file(fileinfo.filename)

    assert addon.name_mapping == {}
    assert addon.file_hashes == {}
    mock_remove.assert_any_call('autosave')
    assert mock_dialog.called == exception


def test_get_autosave_filename(mocker, tmpdir):
    """Test that AutosaveForStack.get_autosave_filename returns a consistent
    and unique name for the autosave file is returned."""
    addon = AutosaveForStack(mocker.Mock())
    mocker.patch('spyder.plugins.editor.utils.autosave.get_conf_path',
                 return_value=str(tmpdir))

    expected = str(tmpdir.join('foo.py'))
    assert addon.get_autosave_filename('foo.py') == expected

    expected2 = str(tmpdir.join('foo-1.py'))
    assert addon.get_autosave_filename('foo.py') == expected
    assert addon.get_autosave_filename('ham/foo.py') == expected2


@pytest.mark.parametrize('have_hash', [True, False])
def test_autosave_file_renamed(mocker, tmpdir, have_hash):
    """Test that AutosaveForStack.file_renamed removes the old autosave file,
    creates a new one, and updates `name_mapping` and `file_hashes`."""
    mock_remove = mocker.patch('os.remove')
    mocker.patch('spyder.plugins.editor.utils.autosave.get_conf_path',
                 return_value=str(tmpdir))
    mock_editor = mocker.Mock()
    mock_fileinfo = mocker.Mock(editor=mock_editor, filename='new_foo.py',
                                newly_created=False)
    mock_document = mocker.Mock()
    mock_fileinfo.editor.document.return_value = mock_document
    mock_stack = mocker.Mock(data=[mock_fileinfo])
    mock_stack.has_filename.return_value = 0
    mock_stack.compute_hash.return_value = 3
    addon = AutosaveForStack(mock_stack)
    old_autosavefile = str(tmpdir.join('old_foo.py'))
    new_autosavefile = str(tmpdir.join('new_foo.py'))
    addon.name_mapping = {'old_foo.py': old_autosavefile}
    addon.file_hashes = {'old_foo.py': 1, old_autosavefile: 42}
    if have_hash:
        addon.file_hashes = {'old_foo.py': 1, old_autosavefile: 42}
    else:
        addon.file_hashes = {old_autosavefile: 42}

    addon.file_renamed('old_foo.py', 'new_foo.py')

    mock_remove.assert_any_call(old_autosavefile)
    mock_stack._write_to_file.assert_called_with(
        mock_fileinfo, new_autosavefile)
    assert addon.name_mapping == {'new_foo.py': new_autosavefile}
    if have_hash:
        assert addon.file_hashes == {'new_foo.py': 1, new_autosavefile: 3}
    else:
        assert addon.file_hashes == {new_autosavefile: 3}


if __name__ == "__main__":
    pytest.main()
