# 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
from unittest.mock import Mock

import pytest
from toolz import assoc_in, compose

from czc import configuration, type_util
from czc.configuration import ORGANIZATION_CONFIG_KEY, ORGANIZATION_DEFAULT, OrganizationConfig
from czc.json import serializable
from czc.type_util import deserializable
from czt.unittest import MATCH, Context, MockEx

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),
    )
