from pathlib import PurePath

import pytest
from PyQt6.QtCore import QDateTime, QItemSelectionModel, Qt
from PyQt6.QtWidgets import QMenu

import vorta.borg
import vorta.utils
import vorta.views.archive_tab
from vorta.store.models import ArchiveModel
from vorta.views.diff_result import (
    ChangeType,
    DiffData,
    DiffResultDialog,
    DiffTree,
    FileType,
    parse_diff_json,
    parse_diff_lines,
)
from vorta.views.partials.treemodel import FileTreeModel


def setup_diff_result_window(qtbot, mocker, tab, borg_json_output, json_mock_file="diff_archives"):
    """Sets up the diff result window."""
    stdout, stderr = borg_json_output(json_mock_file)
    popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0)
    mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result)

    compat = vorta.utils.borg_compat

    def check(feature_name):
        if feature_name == 'DIFF_JSON_LINES':
            return False
        return vorta.utils.BorgCompatibility.check(compat, feature_name)

    mocker.patch.object(vorta.utils.borg_compat, 'check', check)

    selection_model: QItemSelectionModel = tab.archiveTable.selectionModel()
    model = tab.archiveTable.model()

    flags = QItemSelectionModel.SelectionFlag.Rows
    flags |= QItemSelectionModel.SelectionFlag.Select

    selection_model.select(model.index(0, 0), flags)
    selection_model.select(model.index(1, 0), flags)

    tab.diff_action()

    qtbot.waitUntil(lambda: hasattr(tab, '_resultwindow'), **pytest._wait_defaults)
    assert hasattr(tab, '_resultwindow')


@pytest.mark.parametrize(
    'json_mock_file, folder_root', [('diff_archives', 'test'), ('diff_archives_dict_issue', 'Users')]
)
def test_archive_diff(qapp, qtbot, mocker, borg_json_output, json_mock_file, folder_root, archive_env):
    """Tests basic functionality of archive diff."""
    main, tab = archive_env
    setup_diff_result_window(qtbot, mocker, tab, borg_json_output, json_mock_file)

    model = tab._resultwindow.treeView.model().sourceModel()
    assert model.root.children[0].subpath == folder_root
    assert tab._resultwindow.archiveNameLabel_1.text() == 'test-archive'
    tab._resultwindow.accept()


def test_diff_item_copy(qapp, qtbot, mocker, borg_json_output, archive_env):
    """Tests copy action by row selection and when passed an index."""
    main, tab = archive_env
    setup_diff_result_window(qtbot, mocker, tab, borg_json_output)

    # mock the clipboard to ensure no changes are made to it during testing
    mocker.patch.object(qapp.clipboard(), "setMimeData")
    clipboard_spy = mocker.spy(qapp.clipboard(), "setMimeData")

    # test 'diff_item_copy()' by passing it an item to copy
    index = tab._resultwindow.treeView.model().index(0, 0)
    assert index is not None
    tab._resultwindow.diff_item_copy(index)
    clipboard_data = clipboard_spy.call_args[0][0]
    assert clipboard_data.hasText()
    assert clipboard_data.text() == "/test"

    clipboard_spy.reset_mock()

    # test 'diff_item_copy()' by selecting a row to copy
    flags = QItemSelectionModel.SelectionFlag.Rows
    flags |= QItemSelectionModel.SelectionFlag.Select
    tab._resultwindow.treeView.selectionModel().select(tab._resultwindow.treeView.model().index(0, 0), flags)
    tab._resultwindow.diff_item_copy()
    clipboard_data = clipboard_spy.call_args[0][0]
    assert clipboard_data.hasText()
    assert clipboard_data.text() == "/test"


def test_treeview_context_menu(qapp, qtbot, mocker, borg_json_output, archive_env):
    """Tests the diff result window context menu for expected actions."""
    main, tab = archive_env
    setup_diff_result_window(qtbot, mocker, tab, borg_json_output)

    # Load the context menu at the first result in window
    pos = tab._resultwindow.treeView.visualRect(tab._resultwindow.treeView.model().index(0, 0)).center()
    tab._resultwindow.treeview_context_menu(pos)
    qtbot.waitUntil(lambda: tab._resultwindow.findChild(QMenu) is not None, **pytest._wait_defaults)
    context_menu = tab._resultwindow.findChild(QMenu)
    assert context_menu is not None

    # assert the actions are available in the context menu
    expected_actions = ['Copy', 'Expand recursively']
    for action in expected_actions:
        assert any(menu_actions.text() == action for menu_actions in context_menu.actions())


@pytest.mark.parametrize(
    'line, expected',
    [
        (
            'changed link        some/changed/link',
            ('some/changed/link', FileType.LINK, ChangeType.CHANGED_LINK, 0, 0, None, None, None, None, None),
        ),
        (
            ' +77.8 kB  -77.8 kB some/changed/file',
            (
                'some/changed/file',
                FileType.FILE,
                ChangeType.MODIFIED,
                2 * 77800,
                0,
                None,
                None,
                None,
                None,
                (77800, 77800),
            ),
        ),
        (
            ' +77.8 kB  -77.8 kB [-rw-rw-rw- -> -rw-r--r--] some/changed/file',
            (
                'some/changed/file',
                FileType.FILE,
                ChangeType.MODIFIED,
                2 * 77800,
                0,
                ('-rw-rw-rw-', '-rw-r--r--'),
                None,
                None,
                None,
                (77800, 77800),
            ),
        ),
        (
            '[-rw-rw-rw- -> -rw-r--r--] some/changed/file',
            (
                'some/changed/file',
                FileType.FILE,
                ChangeType.MODE,
                0,
                0,
                ('-rw-rw-rw-', '-rw-r--r--'),
                None,
                None,
                None,
                None,
            ),
        ),
        (
            'added directory    some/changed/dir',
            ('some/changed/dir', FileType.DIRECTORY, ChangeType.ADDED, 0, 0, None, None, None, None, None),
        ),
        (
            'removed directory  some/changed/dir',
            ('some/changed/dir', FileType.DIRECTORY, ChangeType.REMOVED_DIR, 0, 0, None, None, None, None, None),
        ),
        # Example from https://github.com/borgbase/vorta/issues/521
        (
            '[user:user -> nfsnobody:nfsnobody] home/user/arrays/test.txt',
            (
                'home/user/arrays/test.txt',
                FileType.FILE,
                ChangeType.OWNER,
                0,
                0,
                None,
                ('user', 'user', 'nfsnobody', 'nfsnobody'),
                None,
                None,
                None,
            ),
        ),
        # Very short owner change, to check stripping whitespace from file path
        (
            '[a:a -> b:b]       home/user/arrays/test.txt',
            (
                'home/user/arrays/test.txt',
                FileType.FILE,
                ChangeType.OWNER,
                0,
                0,
                None,
                ('a', 'a', 'b', 'b'),
                None,
                None,
                None,
            ),
        ),
        # All file-related changes in one test
        (
            ' +77.8 kB  -800 B [user:user -> nfsnobody:nfsnobody] [-rw-rw-rw- -> -rw-r--r--] home/user/arrays/test.txt',
            (
                'home/user/arrays/test.txt',
                FileType.FILE,
                ChangeType.OWNER,
                77800 + 800,
                77000,
                ('-rw-rw-rw-', '-rw-r--r--'),
                ('user', 'user', 'nfsnobody', 'nfsnobody'),
                None,
                None,
                (77800, 800),
            ),
        ),
    ],
)
def test_archive_diff_parser(line, expected):
    model = DiffTree()
    model.setMode(model.DisplayMode.FLAT)
    parse_diff_lines([line], model)

    assert model.rowCount() == 1
    item = model.index(0, 0).internalPointer()

    assert item.path == PurePath(expected[0]).parts
    assert item.data == DiffData(*expected[1:])


@pytest.mark.parametrize(
    'line, expected',
    [
        (
            {'path': 'some/changed/link', 'changes': [{'type': 'changed link'}]},
            ('some/changed/link', FileType.LINK, ChangeType.CHANGED_LINK, 0, 0, None, None, None, None, None),
        ),
        (
            {'path': 'some/changed/file', 'changes': [{'type': 'modified', 'added': 77800, 'removed': 77800}]},
            (
                'some/changed/file',
                FileType.FILE,
                ChangeType.MODIFIED,
                2 * 77800,
                0,
                None,
                None,
                None,
                None,
                (77800, 77800),
            ),
        ),
        (
            {
                'path': 'some/changed/file',
                'changes': [
                    {'type': 'modified', 'added': 77800, 'removed': 800},
                    {'type': 'mode', 'old_mode': '-rw-rw-rw-', 'new_mode': '-rw-r--r--'},
                ],
            },
            (
                'some/changed/file',
                FileType.FILE,
                ChangeType.MODIFIED,
                77800 + 800,
                77000,
                ('-rw-rw-rw-', '-rw-r--r--'),
                None,
                None,
                None,
                (77800, 800),
            ),
        ),
        (
            {
                'path': 'some/changed/file',
                'changes': [{'type': 'mode', 'old_mode': '-rw-rw-rw-', 'new_mode': '-rw-r--r--'}],
            },
            (
                'some/changed/file',
                FileType.FILE,
                ChangeType.MODE,
                0,
                0,
                ('-rw-rw-rw-', '-rw-r--r--'),
                None,
                None,
                None,
                None,
            ),
        ),
        (
            {'path': 'some/changed/dir', 'changes': [{'type': 'added directory'}]},
            ('some/changed/dir', FileType.DIRECTORY, ChangeType.ADDED, 0, 0, None, None, None, None, None),
        ),
        (
            {'path': 'some/changed/dir', 'changes': [{'type': 'removed directory'}]},
            ('some/changed/dir', FileType.DIRECTORY, ChangeType.REMOVED_DIR, 0, 0, None, None, None, None, None),
        ),
        # Example from https://github.com/borgbase/vorta/issues/521
        (
            {
                'path': 'home/user/arrays/test.txt',
                'changes': [
                    {
                        'type': 'owner',
                        'old_user': 'user',
                        'new_user': 'nfsnobody',
                        'old_group': 'user',
                        'new_group': 'nfsnobody',
                    }
                ],
            },
            (
                'home/user/arrays/test.txt',
                FileType.FILE,
                ChangeType.OWNER,
                0,
                0,
                None,
                ('user', 'user', 'nfsnobody', 'nfsnobody'),
                None,
                None,
                None,
            ),
        ),
        # Very short owner change, to check stripping whitespace from file path
        (
            {
                'path': 'home/user/arrays/test.txt',
                'changes': [{'type': 'owner', 'old_user': 'a', 'new_user': 'b', 'old_group': 'a', 'new_group': 'b'}],
            },
            (
                'home/user/arrays/test.txt',
                FileType.FILE,
                ChangeType.OWNER,
                0,
                0,
                None,
                ('a', 'a', 'b', 'b'),
                None,
                None,
                None,
            ),
        ),
        # Short ctime change
        (
            {
                'path': 'home/user/arrays',
                'changes': [
                    {
                        'new_ctime': '2023-04-01T17:23:14.104630',
                        'old_ctime': '2023-03-03T23:40:17.073948',
                        'type': 'ctime',
                    }
                ],
            },
            (
                'home/user/arrays',
                FileType.FILE,
                ChangeType.MODIFIED,
                0,
                0,
                None,
                None,
                (
                    QDateTime.fromString('2023-03-03T23:40:17.073948', Qt.DateFormat.ISODateWithMs),
                    QDateTime.fromString('2023-04-01T17:23:14.104630', Qt.DateFormat.ISODateWithMs),
                ),
                None,
                None,
            ),
        ),
        # Short mtime change
        (
            {
                'path': 'home/user/arrays',
                'changes': [
                    {
                        'new_mtime': '2023-04-01T17:23:14.104630',
                        'old_mtime': '2023-03-03T23:40:17.073948',
                        'type': 'mtime',
                    }
                ],
            },
            (
                'home/user/arrays',
                FileType.FILE,
                ChangeType.MODIFIED,
                0,
                0,
                None,
                None,
                None,
                (
                    QDateTime.fromString('2023-03-03T23:40:17.073948', Qt.DateFormat.ISODateWithMs),
                    QDateTime.fromString('2023-04-01T17:23:14.104630', Qt.DateFormat.ISODateWithMs),
                ),
                None,
            ),
        ),
        # All file-related changes in one test
        (
            {
                'path': 'home/user/arrays/test.txt',
                'changes': [
                    {'type': 'modified', 'added': 77800, 'removed': 77800},
                    {'type': 'mode', 'old_mode': '-rw-rw-rw-', 'new_mode': '-rw-r--r--'},
                    {
                        'type': 'owner',
                        'old_user': 'user',
                        'new_user': 'nfsnobody',
                        'old_group': 'user',
                        'new_group': 'nfsnobody',
                    },
                    {
                        'new_ctime': '2023-04-01T17:23:14.104630',
                        'old_ctime': '2023-03-03T23:40:17.073948',
                        'type': 'ctime',
                    },
                    {
                        'new_mtime': '2023-04-01T17:15:50.290565',
                        'old_mtime': '2023-03-05T00:24:00.359045',
                        'type': 'mtime',
                    },
                ],
            },
            (
                'home/user/arrays/test.txt',
                FileType.FILE,
                ChangeType.OWNER,
                2 * 77800,
                0,
                ('-rw-rw-rw-', '-rw-r--r--'),
                ('user', 'user', 'nfsnobody', 'nfsnobody'),
                (
                    QDateTime.fromString('2023-03-03T23:40:17.073948', Qt.DateFormat.ISODateWithMs),
                    QDateTime.fromString('2023-04-01T17:23:14.104630', Qt.DateFormat.ISODateWithMs),
                ),
                (
                    QDateTime.fromString('2023-03-05T00:24:00.359045', Qt.DateFormat.ISODateWithMs),
                    QDateTime.fromString('2023-04-01T17:15:50.290565', Qt.DateFormat.ISODateWithMs),
                ),
                (77800, 77800),
            ),
        ),
    ],
)
def test_archive_diff_json_parser(line, expected):
    model = DiffTree()
    model.setMode(model.DisplayMode.FLAT)
    parse_diff_json([line], model)

    assert model.rowCount() == 1
    item = model.index(0, 0).internalPointer()

    assert item.path == PurePath(expected[0]).parts
    assert item.data == DiffData(*expected[1:])


@pytest.mark.parametrize(
    "selection, expected_mode, expected_bCollapseAllEnabled",
    [
        (0, FileTreeModel.DisplayMode.TREE, True),
        (1, FileTreeModel.DisplayMode.SIMPLIFIED_TREE, True),
        (2, FileTreeModel.DisplayMode.FLAT, False),
    ],
)
def test_change_display_mode(selection: int, expected_mode, expected_bCollapseAllEnabled):
    dialog = DiffResultDialog(ArchiveModel(), ArchiveModel(), DiffTree())
    dialog.change_display_mode(selection)

    assert dialog.model.mode == expected_mode
    assert dialog.bCollapseAll.isEnabled() == expected_bCollapseAllEnabled
