# -*- coding: utf-8 -*-
# Copyright (c) 2017 CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
from decimal import Decimal
from enum import Enum
from typing import Any, NamedTuple, Optional, Dict, Set
from unittest.mock import Mock

import pytest
from czt.unittest import Context, MockEx, MATCH
from toolz import compose, assoc_in

from czc import configuration, type_util
from czc.configuration import ORGANIZATION_DEFAULT, ORGANIZATION_CONFIG_KEY, OrganizationConfig
from czc.json import serializable
from czc.type_util import deserializable

ORGANIZATION_ID = 'fake-org-id'


class Visibility(Enum):
    show = 'show'
    hide = 'hide'


class ParameterType(str, Enum):
    resources = 'resources'
    region = 'region'
    costcontext = 'costcontext'
    ccf_type = 'ccf_type'


class ConfigElement(str, Enum):
    some_element = 'some_element'


class ConfigElementLevel(str, Enum):
    default = 'default'
    very_high = 'very_high'


def _convertable_to_int(value: Any) -> bool:
    try:
        int(value)
        return True
    except Exception:
        return False


class ComplexObject:
    def __init__(self, value: Any):
        self._value = int(value) if _convertable_to_int(value) else value

    def serializable(self) -> str:
        return self._value

    def __eq__(self, other):
        return self._value == other._value

    def __repr__(self):
        return f'ComplexObject({self._value})'


class ConfigElementLevelOverride(NamedTuple):
    new_value_for_type: Dict[ConfigElement, ConfigElementLevel] = {}


class ObjectConfig(NamedTuple):
    complex_obj: Optional[ComplexObject] = None


class FeatureSwitchConfig(NamedTuple):
    optional_parameter: Optional[Set[ParameterType]] = None


@deserializable()
class TestConfigModel(NamedTuple):
    test_value_override: ConfigElementLevelOverride = ConfigElementLevelOverride()
    object_config: ObjectConfig = ObjectConfig()
    feature_switch_config: FeatureSwitchConfig = FeatureSwitchConfig()


@pytest.fixture(scope='function')
def context(mocker):
    with Context(mocker, configuration, ['feature_data'], extra=dict(
            mock_conn=Mock(),
            mock_feature_data_for_type=Mock(),
    )) as context:
        context.mock_feature_data.connect.return_value = context.mock_conn
        context.mock_feature_data.for_type.return_value = context.mock_feature_data_for_type
        configuration.get_config_cache.clear()
        yield context


def _organization_config() -> OrganizationConfig:
    return configuration.OrganizationConfig(TestConfigModel)


def _config(complex_obj: Any = None, some_element_new_value: str = None, optional_parameter_value: Any = None) -> dict:
    apply_updates = compose(
        lambda x: assoc_in(x, ['test_value_override', 'new_value_for_type', 'some_element'],
                           some_element_new_value) if some_element_new_value else x,
        lambda x: assoc_in(x, ['object_config', 'complex_obj'], complex_obj),
        lambda x: assoc_in(x, ['feature_switch_config', 'optional_parameter'], optional_parameter_value)
    )
    return apply_updates(serializable(TestConfigModel()))


def _mock_feature_data_get(context, org_key: str, return_value: dict):
    MockEx(context.mock_feature_data_for_type.get).if_called_with(
        context.mock_conn,
        org_key,
        ORGANIZATION_CONFIG_KEY).return_value(return_value)


@pytest.mark.unit
@pytest.mark.parametrize(
    'default_config,org_config,expected_config', [
        ({}, {}, TestConfigModel()),
        (_config(), {}, TestConfigModel()),
        ({}, _config(), TestConfigModel()),
        (_config(), _config(complex_obj=10),
         TestConfigModel(object_config=ObjectConfig(complex_obj=ComplexObject(10)))),
        (_config(complex_obj=10), _config(),
         TestConfigModel(object_config=ObjectConfig(complex_obj=ComplexObject(10)))),
        (_config(complex_obj=6), _config(complex_obj=12), TestConfigModel(
            object_config=ObjectConfig(complex_obj=ComplexObject(12)))),
        (_config(complex_obj=12), _config(complex_obj=6), TestConfigModel(
            object_config=ObjectConfig(complex_obj=ComplexObject(6)))),
        (_config(some_element_new_value='very_high'), _config(), TestConfigModel(
            test_value_override=ConfigElementLevelOverride(
                new_value_for_type={ConfigElement.some_element: ConfigElementLevel.very_high}))),
        (_config(), _config(some_element_new_value='very_high'), TestConfigModel(
            test_value_override=ConfigElementLevelOverride(
                new_value_for_type={ConfigElement.some_element: ConfigElementLevel.very_high}))),
        (_config(some_element_new_value='default'), _config(some_element_new_value='very_high'),
         TestConfigModel(test_value_override=ConfigElementLevelOverride(
             new_value_for_type={ConfigElement.some_element: ConfigElementLevel.very_high}))),
        (_config(some_element_new_value='very_high'), _config(some_element_new_value='default'),
         TestConfigModel(test_value_override=ConfigElementLevelOverride(
             new_value_for_type={ConfigElement.some_element: ConfigElementLevel.default}))),
        (_config(complex_obj=33), _config(some_element_new_value='default'),
         TestConfigModel(
             object_config=ObjectConfig(complex_obj=ComplexObject(33)),
             test_value_override=ConfigElementLevelOverride(
                 new_value_for_type={ConfigElement.some_element: ConfigElementLevel.default}))),
        (_config(complex_obj=33), _config(complex_obj=5, some_element_new_value='default'),
         TestConfigModel(
             object_config=ObjectConfig(complex_obj=ComplexObject(5)),
             test_value_override=ConfigElementLevelOverride(
                 new_value_for_type={ConfigElement.some_element: ConfigElementLevel.default}))),
        (_config(complex_obj=33), _config(complex_obj='some string value', some_element_new_value='default'),
         TestConfigModel(
             object_config=ObjectConfig(complex_obj=ComplexObject('some string value')),
             test_value_override=ConfigElementLevelOverride(
                 new_value_for_type={ConfigElement.some_element: ConfigElementLevel.default}))),
        (_config(), _config(complex_obj=Decimal('5')),
         TestConfigModel(
             object_config=ObjectConfig(complex_obj=ComplexObject(5)))),
        (_config(complex_obj=33), _config(complex_obj=[42, 'some string value']),
         TestConfigModel(
             object_config=ObjectConfig(complex_obj=ComplexObject([42, 'some string value'])))),
        (_config(complex_obj=[1, 2]), _config(some_element_new_value='default'),
         TestConfigModel(
             object_config=ObjectConfig(complex_obj=ComplexObject([1, 2])),
             test_value_override=ConfigElementLevelOverride(
                 new_value_for_type={ConfigElement.some_element: ConfigElementLevel.default}))),
        (_config(optional_parameter_value=['resources']), _config(optional_parameter_value=['region', 'ccf_type']),
         TestConfigModel(
             feature_switch_config=FeatureSwitchConfig(
                 optional_parameter={ParameterType.region, ParameterType.ccf_type}
             ))),
        (_config(optional_parameter_value=['resources']), _config(),
         TestConfigModel(
             feature_switch_config=FeatureSwitchConfig(
                 optional_parameter={ParameterType.resources}
             ))),
    ])
def test_get_organization_config_should_merge_org_and_default(context, default_config, org_config, expected_config):
    _mock_feature_data_get(context, ORGANIZATION_DEFAULT, default_config)
    _mock_feature_data_get(context, ORGANIZATION_ID, org_config)

    result = _organization_config().get(ORGANIZATION_ID)
    assert result == expected_config


def _assert_set_config(context, expected_config: TestConfigModel,
                       organization_key: str = ORGANIZATION_ID,
                       current_config: TestConfigModel = None):
    def validate_handler(h):
        return h(current_config or TestConfigModel()) == expected_config

    assert MockEx(context.mock_feature_data_for_type.update).was_called_once_with(
        context.mock_conn,
        organization_key,
        ORGANIZATION_CONFIG_KEY,
        handler=MATCH(validate_handler))


@pytest.mark.unit
@pytest.mark.parametrize(
    'organization_id,expected_org_key', [
        (None, ORGANIZATION_DEFAULT),
        ('111-222-333', '111-222-333'),
    ])
def test_init_config_should_set_empty_config_for_org(context, organization_id, expected_org_key):
    _organization_config().init(organization_id)
    _assert_set_config(context, TestConfigModel(), organization_key=expected_org_key)


@pytest.mark.unit
@pytest.mark.parametrize(
    'defaults,current_config,expected_config', [
        (None, {}, _config()),
        (None, dict(object_config=dict(complex_obj=3)), _config(complex_obj=3)),
        (TestConfigModel(), dict(object_config=dict(complex_obj=3)), _config(complex_obj=3)),
        (TestConfigModel(object_config=ObjectConfig(complex_obj=ComplexObject(10))),
         dict(object_config=dict(complex_obj=3)), _config(complex_obj=3)),
        (TestConfigModel(object_config=ObjectConfig(complex_obj=ComplexObject(10))),
         {}, _config(complex_obj=10)),
        (TestConfigModel(object_config=ObjectConfig(complex_obj=ComplexObject([10, 'a different value']))),
         {}, _config(complex_obj=[10, 'a different value'])),
        (TestConfigModel(test_value_override=ConfigElementLevelOverride(
            new_value_for_type={ConfigElement.some_element: ConfigElementLevel.very_high})),
         _config(complex_obj=10), _config(complex_obj=10, some_element_new_value='very_high')),
        (TestConfigModel(test_value_override=ConfigElementLevelOverride(
            new_value_for_type={ConfigElement.some_element: ConfigElementLevel.very_high})),
         _config(complex_obj='a different value'),
         _config(complex_obj='a different value', some_element_new_value='very_high')),
        (TestConfigModel(test_value_override=ConfigElementLevelOverride(
            new_value_for_type={ConfigElement.some_element: ConfigElementLevel.very_high})),
         _config(complex_obj=[24, 'a different value']), _config(complex_obj=[24, 'a different value'],
                                                                 some_element_new_value='very_high')),
    ])
def test_init_config_should_not_destroy_existing_values(context, defaults, current_config, expected_config):
    _organization_config().init(ORGANIZATION_ID, defaults)
    _assert_set_config(context,
                       type_util.coerce(TestConfigModel, expected_config),
                       current_config=type_util.coerce(TestConfigModel, current_config))
