# SPDX-FileCopyrightText: 2022 Chris Heywood
#
# SPDX-License-Identifier: GPL-3.0-or-later

import json
import os
import shutil
import time
from typing import Optional
import unittest
import warnings

from iotas.backup_manager import BackupManager
from iotas.backup_storage import BackupStorage
from iotas.note import Note, DirtyFields

warnings.filterwarnings("ignore", "version")


class _MockStorage(BackupStorage):

    notes: list[Note] = []

    def add_note(self, note: Note) -> None:
        """Add a new note to the database.

        :param Note note: Note to add
        """
        self.notes.append(note)

    def get_all_notes_count(self, include_locally_deleted: bool = True) -> int:
        """Fetch the number of notes in the database.

        :param bool include_locally_deleted: Whether to include locally deleted notes in the count
        :return: The count
        :rtype: int
        """
        return len(self.notes)

    def get_all_notes(self, load_content: bool = False) -> list[Note]:
        """Fetch all notes from the database.

        :param bool load_content: Whether to load the content for each note
        :return: List of notes
        :rtype: list[Note]
        """
        return self.notes[:]

    def get_note_by_remote_id(self, remote_id: int) -> Optional[Note]:
        """Fetch note from the database by remote id.

        :param int remote_id: Remote id of the note
        :return: The note, or None
        :rtype: Optional[Note]
        """
        result = None
        for note in self.notes:
            if note.remote_id == remote_id:
                result = note
                break
        return result

    def get_note_by_title(self, title: str) -> Optional[Note]:
        """Fetch note from the database by title.

        :param str title: Search title
        :return: The note, or None
        :rtype: Optional[Note]
        """
        result = None
        for note in self.notes:
            if note.title == title:
                result = note
                break
        return result

    def persist_note_selective(self, note: Note, updated_fields: DirtyFields) -> None:
        """Persist local note based on remote changes.

        :param Note note: The note to update
        :param DirtyFields updated_fields: Which fields to update
        """
        # Not implementing for unit test due to backup merging being disabled
        raise NotImplementedError()

    def create_duplicate_note(self, note: Note, reason: str) -> Note:
        """Create a duplicate note with a prefixed title.

        :param Note note: The note to duplicate
        :param str reason: The reason for the duplication, which is prefixed on the title
        """
        # Not implementing for unit test due to backup merging being disabled
        raise NotImplementedError()

    def flush(self) -> None:
        self.notes = []


class Test(unittest.TestCase):

    BACKUPS_SUBPATH = "backup"

    mock_storage = _MockStorage()

    def test_create_backup(self) -> None:
        self.__reset()
        backups_dir = os.path.join(self.__get_output_dir(), self.BACKUPS_SUBPATH)

        note1 = Note(new_note=True)
        note1.content = "content"
        note1.title = "Test Note Title"
        self.mock_storage.add_note(note1)

        note2 = Note(new_note=True)
        note2.content = "second content"
        note2.title = "Second Note Title"
        self.mock_storage.add_note(note2)

        result = self.manager.create_backup(sync_configured=False, file_extension="md")
        self.assertTrue(result)

        files = os.listdir(backups_dir)
        self.assertEqual(len(files), 4)
        self.assertTrue(f"{note1.title}.md" in files)
        self.assertTrue(f"{note1.title}.md.iota" in files)
        self.assertTrue(f"{note2.title}.md" in files)
        self.assertTrue(f"{note2.title}.md.iota" in files)

        with open(os.path.join(backups_dir, f"{note1.title}.md"), "r") as f:
            self.assertEqual(note1.content, f.read())

        with open(os.path.join(backups_dir, f"{note1.title}.md.iota"), "r") as f:
            meta_dict = json.loads(f.read())
        self.assertEqual(meta_dict["SchemaVersion"], "1.0")
        self.assertEqual(meta_dict["IotasVersion"], "0.0")
        self.assertEqual(meta_dict["Title"], note1.title)
        self.assertEqual(meta_dict["Category"], note1.category)
        self.assertEqual(meta_dict["LastModified"], note1.last_modified)
        self.assertEqual(meta_dict["Favourite"], note1.favourite)
        self.assertEqual(meta_dict["Dirty"], note1.dirty)
        self.assertEqual(meta_dict["LocallyDeleted"], note1.locally_deleted)
        self.assertEqual(meta_dict["RemoteId"], note1.remote_id)
        self.assertEqual(meta_dict["ETag"], note1.etag)

        self.__reset()

        note = Note(new_note=True)
        note.content = "content"
        note.title = "Test Note Title"
        note.category = "category"
        note.last_modified = int(time.time())
        note.favourite = True
        note.dirty = True
        note.locally_deleted = True
        note.remote_id = 13
        note.etag = "etag"
        self.mock_storage.add_note(note)

        result = self.manager.create_backup(sync_configured=False, file_extension="md")
        self.assertTrue(result)

        file_path = os.path.join(backups_dir, f"{note.title}.md.iota")
        self.assertTrue(os.path.exists(file_path))

        with open(file_path, "r") as f:
            meta_dict = json.loads(f.read())
        self.assertEqual(meta_dict["SchemaVersion"], "1.0")
        self.assertEqual(meta_dict["IotasVersion"], "0.0")
        self.assertEqual(meta_dict["Title"], note.title)
        self.assertEqual(meta_dict["Category"], note.category)
        self.assertEqual(meta_dict["LastModified"], note.last_modified)
        self.assertEqual(meta_dict["Favourite"], note.favourite)
        self.assertEqual(meta_dict["Dirty"], note.dirty)
        self.assertEqual(meta_dict["LocallyDeleted"], note.locally_deleted)
        self.assertEqual(meta_dict["RemoteId"], note.remote_id)
        self.assertEqual(meta_dict["ETag"], note.etag)

        self.__clean_output_dir()

    def test_restore_backup(self) -> None:
        self.__reset()
        backups_dir = os.path.join(self.__get_output_dir(), self.BACKUPS_SUBPATH)
        os.makedirs(backups_dir)
        with open(os.path.join(backups_dir, "Title.md"), "w") as f:
            f.write("content")
        with open(os.path.join(backups_dir, "Title.md.iota"), "w") as f:
            f.write("""{
                  "SchemaVersion": "1.0",
                  "IotasVersion": "0.0",
                  "Title": "Title",
                  "Category": "",
                  "LastModified": 0,
                  "Favourite": false,
                  "Dirty": false,
                  "LocallyDeleted": false,
                  "RemoteId": -1,
                  "ETag": ""
                }""")
        with open(os.path.join(backups_dir, "Second Title.md"), "w") as f:
            f.write("second content")
        with open(os.path.join(backups_dir, "Second Title.md.iota"), "w") as f:
            f.write("""{
                  "SchemaVersion": "1.0",
                  "IotasVersion": "0.0",
                  "Title": "Second Title",
                  "Category": "category",
                  "LastModified": 1745975049,
                  "Favourite": true,
                  "Dirty": true,
                  "LocallyDeleted": true,
                  "RemoteId": 13,
                  "ETag": "etag"
                }""")
        result = self.manager.restore_backup(sync_configured=False)
        self.assertTrue(result)
        self.assertEqual(self.mock_storage.get_all_notes_count(True), 2)

        note = self.mock_storage.get_note_by_title("Title")
        self.assertIsNotNone(note)
        assert note  # mypy
        self.assertEqual(note.title, "Title")
        self.assertEqual(note.content, "content")
        self.assertEqual(note.category, "")
        self.assertEqual(note.last_modified, 0)
        self.assertEqual(note.favourite, False)
        self.assertEqual(note.dirty, False)
        self.assertEqual(note.locally_deleted, False)
        self.assertEqual(note.remote_id, -1)
        self.assertEqual(note.etag, "")

        note = self.mock_storage.get_note_by_title("Second Title")
        self.assertIsNotNone(note)
        assert note  # mypy
        self.assertEqual(note.title, "Second Title")
        self.assertEqual(note.content, "second content")
        self.assertEqual(note.category, "category")
        self.assertEqual(note.last_modified, 1745975049)
        self.assertEqual(note.favourite, True)
        self.assertEqual(note.dirty, True)
        self.assertEqual(note.locally_deleted, True)

        # Not populated during restore
        self.assertEqual(note.remote_id, -1)
        self.assertEqual(note.etag, "")

        result = self.manager.restore_backup(sync_configured=False)
        self.assertFalse(result)

        self.__clean_output_dir()

    def __reset(self) -> BackupManager:
        self.mock_storage.flush()
        self.__prepare_output_dir()
        file_dir = os.path.dirname(__file__)
        storage_dir = os.path.join(file_dir, os.pardir, "testing-tmp")
        self.manager = BackupManager(
            self.mock_storage,
            iotas_version="0.0",
            storage_dir=storage_dir,
        )
        return self.manager

    def __prepare_output_dir(self) -> str:
        out_path = self.__get_output_dir()
        if os.path.exists(out_path):
            shutil.rmtree(out_path)
        os.makedirs(out_path)
        return out_path

    def __clean_output_dir(self) -> None:
        out_path = self.__get_output_dir()
        if os.path.exists(out_path):
            shutil.rmtree(out_path)

    def __get_output_dir(self) -> str:
        file_dir = os.path.dirname(__file__)
        return os.path.join(file_dir, os.pardir, "testing-tmp")
