

import json
import shutil
from pathlib import Path

import pytest

from nucypher.config.base import BaseConfiguration
from nucypher.config.constants import DEFAULT_CONFIG_ROOT

#
# Local Test Constants
#

configuration_name = 'something'
expected_extension = 'json'
configuration_value = 're-emerging llamas'
modifier = '1'
manual_expected_default_filepath = Path('/', 'tmp', 'something.json')
manual_expected_modified_filepath = Path('/', 'tmp', 'something-1.json')


@pytest.fixture(scope='function', autouse=True)
def expected_configuration_filepaths():

    # Setup
    if manual_expected_default_filepath.exists():
        manual_expected_default_filepath.unlink()
    if manual_expected_modified_filepath.exists():
        manual_expected_modified_filepath.unlink()

    yield manual_expected_default_filepath, manual_expected_modified_filepath

    # Teardown
    if manual_expected_default_filepath.exists():
        manual_expected_default_filepath.unlink()
    if manual_expected_modified_filepath.exists():
        manual_expected_modified_filepath.unlink()


class RestorableTestItem(BaseConfiguration):

    NAME = 'something'
    DEFAULT_CONFIG_ROOT = Path('/tmp')
    VERSION = 1

    def __init__(self, item: str, *args, **kwargs):
        self.item = item
        super().__init__(*args, **kwargs)

    def static_payload(self) -> dict:
        payload = dict(**super().static_payload(),
                       item=self.item)
        return payload


def test_base_configuration_defaults():
    assert BaseConfiguration.DEFAULT_CONFIG_ROOT == DEFAULT_CONFIG_ROOT
    assert BaseConfiguration.NAME == NotImplemented
    assert BaseConfiguration._CONFIG_FILE_EXTENSION == expected_extension


def test_configuration_implementation():

    # Cannot init BaseClass without subclassing
    with pytest.raises(TypeError):
        _bad_item = BaseConfiguration()

    # Subclasses must implement static_payload specification
    class BadConfigurableItem(BaseConfiguration):
        VERSION = 1
        pass

    with pytest.raises(TypeError):
        _bad_item = BadConfigurableItem()

    # Subclasses must implement _NAME
    class NoNameItem(BaseConfiguration):
        VERSION = 1
        def static_payload(self) -> dict:
            item_payload = {'key': 'value'}
            payload = {**super().static_payload(), **item_payload}
            return payload

    with pytest.raises(TypeError):
        _bad_item = NoNameItem()

    # Correct minimum viable implementation
    class BareMinimumConfigurableItem(BaseConfiguration):

        NAME = 'bare-minimum'
        VERSION = 2

        def static_payload(self) -> dict:
            item_payload = {'key': 'value'}
            payload = {**super().static_payload(), **item_payload}
            return payload

    _bare_minimum = BareMinimumConfigurableItem()


def test_configuration_creation():
    restorable_item = RestorableTestItem(item=configuration_value)
    assert restorable_item.config_root == RestorableTestItem.DEFAULT_CONFIG_ROOT
    assert restorable_item.item == configuration_value


def test_configuration_filepath_utilities():

    #
    # Class-Scoped
    #

    assert RestorableTestItem._CONFIG_FILE_EXTENSION == expected_extension

    assert RestorableTestItem.NAME == configuration_name
    expected_default_filename = f'{RestorableTestItem.NAME}.{RestorableTestItem._CONFIG_FILE_EXTENSION}'
    assert RestorableTestItem.generate_filename() == expected_default_filename

    expected_default_filepath = RestorableTestItem.DEFAULT_CONFIG_ROOT / expected_default_filename
    assert expected_default_filepath == manual_expected_default_filepath
    assert RestorableTestItem.default_filepath() == expected_default_filepath

    restorable_item = RestorableTestItem(item=configuration_value)  # <-- CREATE
    restorable_item.to_configuration_file()

    #
    # Instance-scoped
    #

    # Ensure filename and filepath construction accuracy
    expected_modified_filename = f'{RestorableTestItem.NAME}-{modifier}.{RestorableTestItem._CONFIG_FILE_EXTENSION}'
    modified_filename = restorable_item.generate_filename(modifier=modifier)
    assert modified_filename == expected_modified_filename

    expected_modified_filepath = RestorableTestItem.DEFAULT_CONFIG_ROOT / expected_modified_filename
    modified_filepath = restorable_item.generate_filepath(override=False, modifier=modifier)
    assert modified_filepath == expected_modified_filepath

    # Ensure Positive Override Control
    filepath = restorable_item.to_configuration_file(override=False, modifier=modifier)
    assert filepath == expected_modified_filepath


def test_configuration_preservation():

    # Create
    restorable_item = RestorableTestItem(item=configuration_value)

    expected_default_filename = f'{RestorableTestItem.NAME}.{RestorableTestItem._CONFIG_FILE_EXTENSION}'
    expected_default_filepath = RestorableTestItem.DEFAULT_CONFIG_ROOT / expected_default_filename

    # Serialize
    assert restorable_item.serialize()
    assert restorable_item.static_payload()

    # Write to JSON file
    filepath = restorable_item.to_configuration_file()
    assert filepath == expected_default_filepath

    # Ensure controlled failure to override
    with pytest.raises(FileExistsError):
        _filepath = restorable_item.to_configuration_file(override=False)

    # Ensure controlled override
    filepath = restorable_item.to_configuration_file(override=True)
    assert filepath == expected_default_filepath

    # Restore from JSON Configuration
    try:

        # Ensure configuration file is readable
        with open(restorable_item.filepath, 'r') as f:
            contents = f.read()

        # Ensure file contents are JSON deserializable
        deserialized_file_contents = json.loads(contents)
        del deserialized_file_contents['version']  # do not test version of config serialization here.
        deserialized_file_contents['config_root'] = Path(deserialized_file_contents['config_root'])

        deserialized_payload = RestorableTestItem.deserialize(payload=contents)
        assert deserialized_payload == deserialized_file_contents

        # Restore from JSON file
        restored_item = RestorableTestItem.from_configuration_file()
        assert restorable_item.serialize() == restored_item.serialize()
        assert restorable_item.item == configuration_value
        assert restorable_item.filepath == expected_default_filepath

    finally:
        shutil.rmtree(restorable_item.filepath, ignore_errors=True)
