"""
Test the implementations of the default functions in Component
"""

# Standard
from unittest import mock
import os
import tempfile

# Third Party
import pytest
import yaml

# First Party
import alog

# Local
from oper8 import constants
from oper8.component import Component, ManagedObject
from oper8.constants import TEMPORARY_PATCHES_ANNOTATION_NAME
from oper8.deploy_manager import DeployMethod
from oper8.deploy_manager.dry_run_deploy_manager import DryRunDeployManager
from oper8.deploy_manager.owner_references import _make_owner_reference
from oper8.patch import STRATEGIC_MERGE_PATCH
from oper8.test_helpers.helpers import (
    TEST_NAMESPACE,
    DummyNodeComponent,
    MockDeployManager,
    configure_logging,
    library_config,
    make_patch,
    setup_session,
)
from oper8.utils import merge_configs

configure_logging()
log = alog.use_channel("TEST")


def get_comp_type(name="dummy"):
    """Parametrization helper to get test both standard and legacy components.
    This function also wraps the output class type so that name class attributes
    are not polluted.
    """
    given_name = name

    class Wrapped(DummyNodeComponent):
        name = given_name

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

    return Wrapped


################################################################################
## Tests #######################################################################
################################################################################

##################
## Construction ##
##################


def test_name_validation():
    """Validate that the name validation passes for a class with a valid
    name class attribute
    """

    class NamedComponent(Component):
        name = "foo"

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

        def build_chart(self, session):
            pass

    class UnnamedComponent(Component):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)

        def build_chart(self, session):
            pass

    session = setup_session()
    component = NamedComponent(session=session)
    assert component.name == NamedComponent.name
    with pytest.raises(NotImplementedError):
        UnnamedComponent(session=session)


def test_scope():
    """Validate that the scope given to the constructor gets wired into the
    component's hierarchy correctly
    """
    session = setup_session()
    comp = get_comp_type()(session=session)
    assert session.get_component(comp.name)


##################
## render_chart ##
##################


def test_managed_objects():
    """Make sure managed_objects matches the objects generated by the
    chart
    """
    session = setup_session()
    comp = get_comp_type()(
        session=session,
        api_objects=[
            ("bar", {"kind": "Foo", "metadata": {"name": "bar"}}),
            ("bat", {"kind": "Baz", "metadata": {"name": "bat"}}),
        ],
    )
    comp.render_chart(session)
    managed_objs = comp.managed_objects
    assert len(managed_objs) == 2
    assert managed_objs[0].kind == "Foo"
    assert managed_objs[1].kind == "Baz"


def test_apply_patches_ok():
    """Make sure applying patches modifies the managed objects as expected"""
    patch_name = "test"
    patch = make_patch(
        STRATEGIC_MERGE_PATCH, {"dummy": {"bar": {"new": "value"}}}, patch_name
    )
    log.debug("Patch: %s", patch)
    dm = DryRunDeployManager(resources=[patch])
    session = setup_session(temporary_patches=[patch])
    bar = {"kind": "Foo", "metadata": {"name": "bar"}}
    bat = {"kind": "Bat", "metadata": {"name": "bat"}}
    with library_config(internal_name_annotation=False):
        comp = get_comp_type()(
            session=session, api_objects=[("bar", bar), ("bat", bat)]
        )
        comp.render_chart(session)
        objs = comp.to_config(session)
    assert len(objs) == 2
    expected_bar = merge_configs(
        bar,
        {
            "new": "value",
            "apiVersion": "v1",
            "metadata": {"namespace": TEST_NAMESPACE},
        },
    )
    assert objs[0] == expected_bar
    assert objs[1] == merge_configs(
        bat, {"apiVersion": "v1", "metadata": {"namespace": TEST_NAMESPACE}}
    )


def test_object_update():
    """Make sure in-place object updates work with update_object_definition"""
    session = setup_session()
    bar = {"kind": "Foo", "metadata": {"name": "bar"}}
    bat = {"kind": "Bat", "metadata": {"name": "bat"}}
    comp = get_comp_type()(session=session, api_objects=[("bar", bar), ("bat", bat)])

    def update_object_definition(session, internal_name, resource_definition):
        if internal_name == "dummy.bar":
            resource_definition["new"] = "value"
        return resource_definition

    comp.update_object_definition = update_object_definition
    with library_config(internal_name_annotation=False):
        comp.render_chart(session)
        objs = comp.to_config(session)
    assert len(objs) == 2
    expected_bar = merge_configs(
        bar,
        {
            "new": "value",
            "apiVersion": "v1",
            "metadata": {"namespace": TEST_NAMESPACE},
        },
    )
    assert objs[0] == expected_bar
    assert objs[1] == merge_configs(
        bat, {"apiVersion": "v1", "metadata": {"namespace": TEST_NAMESPACE}}
    )


def test_add_namespace():
    """Make sure that all internal components auto add the session.namespace to
    all resources
    """
    session = setup_session(namespace="unique")
    bar = {"kind": "Foo", "apiVersion": "v1", "metadata": {"name": "bar"}}

    class TestComponent(Component):
        name = "bar"

        def build_chart(self, session):
            self.add_resource("bar", bar)

    comp = TestComponent(session=session)

    with library_config(internal_name_annotation=True):
        comp.render_chart(session)
        objs = comp.to_config(session)
    assert len(objs) == 1
    assert objs[0].metadata.namespace == "unique"


def test_deploy_method():
    """Make sure that all internal components auto add the session.namespace to
    all resources
    """
    session = setup_session(namespace="unique")
    bar = {"kind": "Foo", "apiVersion": "v1", "metadata": {"name": "bar"}}
    bat = {"kind": "Foo", "apiVersion": "v1", "metadata": {"name": "bat"}}

    class TestComponent(Component):
        name = "bar"

        def build_chart(self, session):
            self.add_resource("replaced_bar", bar, deploy_method=DeployMethod.REPLACE)
            self.add_resource("replaced_bat", bat, deploy_method=DeployMethod.UPDATE)

    comp = TestComponent(session=session)

    arg_kwarg_list = []

    def append_call_args(*args, **kwargs):
        arg_kwarg_list.append([args, kwargs])
        return (True, True)

    with mock.patch.object(
        session.deploy_manager, "deploy", new=append_call_args
    ) as deployed:
        with library_config(internal_name_annotation=True):
            comp.render_chart(session)
            success = comp.deploy(session)

        assert arg_kwarg_list[0][1]["method"] == DeployMethod.REPLACE
        assert arg_kwarg_list[1][1]["method"] == DeployMethod.UPDATE

    assert success


def test_custom_resource_verify():
    """Make sure that a user  can override the verify function for a particular resource."""
    session = setup_session()
    bar = {"kind": "Foo", "apiVersion": "v1", "metadata": {"name": "bar"}}

    class TestSuccessVerify(Component):
        name = "bar"

        def build_chart(self, session):
            self.add_resource("bar", bar)

    class TestFailedVerify(Component):
        name = "bar_fail"

        def build_chart(self, session):
            self.add_resource("bar", bar, lambda resource: False)

    suc_comp = TestSuccessVerify(session=session)
    failed_comp = TestFailedVerify(session=session)

    with library_config(internal_name_annotation=True):
        suc_comp.render_chart(session)
        suc_comp.deploy(session)
        assert suc_comp.verify(session)

        failed_comp.render_chart(session)
        failed_comp.deploy(session)
        assert not failed_comp.verify(session)


def test_internal_name_annotation():
    """Make sure the internal name annotation is added to output resources if
    configured
    """
    session = setup_session()
    bar = {"kind": "Foo", "metadata": {"name": "bar"}}
    CompType = get_comp_type()
    comp = CompType(session=session, api_objects=[("bar", bar)])

    with library_config(internal_name_annotation=True):
        comp.render_chart(session)
        objs = comp.to_config(session)
    assert len(objs) == 1
    assert objs[0].metadata.annotations[
        constants.INTERNAL_NAME_ANNOTATION_NAME
    ] == ".".join([CompType.name, "bar"])


def test_cross_namespace_patch_annotation():
    """Make sure the temporary patch annotation is not added to resources of the same
    name in different namespaces
    """
    # Setup Session
    session = setup_session()
    foo = {
        "kind": "Foo",
        "apiVersion": "v1",
        "metadata": {
            "name": "foo",
            "namespace": session.namespace,
            "annotations": {
                TEMPORARY_PATCHES_ANNOTATION_NAME: "temp_patch",
            },
        },
    }
    bar = {
        "kind": "Foo",
        "api_version": "v1",
        "metadata": {"name": "foo", "namespace": "adifferentnamespace"},
    }

    # Deploy Foo and Remove apiVersion from dict
    session.deploy_manager.deploy([foo])
    del foo["apiVersion"]

    # Setup Component
    comp = get_comp_type()(
        session=session,
        api_objects=[
            ("foo", foo),
            ("bar", bar),
        ],
    )
    rendered = comp.to_dict(session)

    # Validate objects weren't incorrectly patched
    assert len(rendered) == 2
    assert rendered[0]["metadata"]["namespace"] == "adifferentnamespace"
    assert not rendered[0]["metadata"]["annotations"].get(
        TEMPORARY_PATCHES_ANNOTATION_NAME, None
    )
    assert rendered[1]["metadata"]["namespace"] == session.namespace
    assert (
        rendered[1]["metadata"]["annotations"][TEMPORARY_PATCHES_ANNOTATION_NAME]
        == "temp_patch"
    )


def test_dependency_order():
    """Make sure that objects are rendered in the correct order based on the
    declared dependencies in the component.
    """
    session = setup_session()
    comp = get_comp_type()(
        session=session,
        api_objects=[
            ("bar", {"kind": "Foo", "metadata": {"name": "bar"}}),
            ("bat", {"kind": "Baz", "metadata": {"name": "bat"}}),
        ],
        api_object_deps={"bar": ["bat"]},
    )
    comp.render_chart(session)
    managed_objs = comp.managed_objects
    assert len(managed_objs) == 2
    assert managed_objs[0].kind == "Baz"
    assert managed_objs[1].kind == "Foo"


def test_owner_references_external():
    """Make sure that external owner references are preserved"""
    owner_cr = {
        "kind": "Owner",
        "apiVersion": "foo.bar.com/v1",
        "metadata": {
            "name": "owner",
            "namespace": "test",
            "uid": "unique",
        },
    }
    dm = DryRunDeployManager(
        resources=[
            {
                "kind": "Foo",
                "apiVersion": "v1",
                "metadata": {
                    "name": "foo",
                    "namespace": "test",
                    "uid": "new york",
                    "ownerReferences": [
                        {
                            "uid": "external",
                        }
                    ],
                },
            }
        ],
        owner_cr=owner_cr,
    )
    session = setup_session(deploy_manager=dm, namespace="test")
    comp = get_comp_type()(
        session=session,
        api_objects=[("foo", {"kind": "Foo", "api_version": "v1"})],
        full_cr=owner_cr,
    )
    comp.render_chart(session)
    comp.deploy(session)
    _, rendered = session.get_object_current_state(kind="Foo", name="foo")
    assert rendered is not None
    assert len(rendered["metadata"]["ownerReferences"]) == 2


def test_owner_references_no_duplicates():
    """Make sure that the current owner reference is not duplicated"""
    owner_cr = {
        "kind": "Owner",
        "apiVersion": "foo.bar.com/v1",
        "metadata": {
            "name": "owner",
            "namespace": "test",
            "uid": "unique",
        },
    }
    dm = DryRunDeployManager(
        resources=[
            {
                "kind": "Foo",
                "apiVersion": "v1",
                "metadata": {
                    "name": "foo",
                    "namespace": "test",
                    "ownerReferences": [_make_owner_reference(owner_cr)],
                    "uid": "new york",
                },
            }
        ],
        owner_cr=owner_cr,
    )
    session = setup_session(deploy_manager=dm, full_cr=owner_cr, namespace="test")
    comp = get_comp_type()(
        session=session,
        api_objects=[("foo", {"kind": "Foo", "api_version": "v1"})],
        full_cr=owner_cr,
    )
    comp.render_chart(session)
    comp.deploy(session)
    _, rendered = session.get_object_current_state(kind="Foo", name="foo")
    assert rendered is not None
    assert len(rendered["metadata"]["ownerReferences"]) == 1


############
## deploy ##
############


def test_deploy_after_render():
    """Make sure that a deploy run after a render runs cleanly"""
    session = setup_session()
    comp = get_comp_type()(
        session=session,
        api_objects=[
            ("bar", {"kind": "Foo", "metadata": {"name": "bar"}}),
            ("bat", {"kind": "Baz", "metadata": {"name": "bat"}}),
        ],
    )
    comp.render_chart(session)
    assert comp.deploy(session)


def test_deploy_before_render():
    """Make sure that a deploy run before a render throws an appropriate
    exception
    """
    session = setup_session()
    comp = get_comp_type()(session=session)
    with pytest.raises(AssertionError):
        comp.deploy(session)


def test_deploy_failure():
    """Make sure that a failed deploy is captured successfully"""
    session = setup_session(deploy_manager=MockDeployManager(deploy_fail=True))
    comp = get_comp_type()(
        session=session,
        api_objects=[
            ("bar", {"kind": "Foo", "metadata": {"name": "bar"}}),
        ],
    )
    comp.render_chart(session)
    deploy_success = comp.deploy(session)
    assert not deploy_success


def test_deploy_raise():
    """Make sure that a thrown exception in a deploy will raise outside of
    the deploy function (to be handled by the rollout manager)
    """
    session = setup_session(deploy_manager=MockDeployManager(deploy_raise=True))
    comp = get_comp_type()(
        session=session,
        api_objects=[
            ("bar", {"kind": "Foo", "metadata": {"name": "bar"}}),
        ],
    )
    comp.render_chart(session)
    with pytest.raises(AssertionError):
        comp.deploy(session)


################
## Dependency ##
################


def test_add_dependency():
    session = setup_session(deploy_manager=MockDeployManager(deploy_raise=True))
    comp_a = get_comp_type("a")(session=session)
    comp_b = get_comp_type("b")(session=session)
    comp_a.add_dependency(session, comp_b)
    assert session.get_component_dependencies(comp_a) == [(comp_b, None)]


def test_add_multiple_dependency():
    session = setup_session(deploy_manager=MockDeployManager(deploy_raise=True))
    comp_a = get_comp_type("a")(session=session)
    comp_b = get_comp_type("b")(session=session)
    comp_c = get_comp_type("c")(session=session)
    comp_a.add_dependency(session, comp_b, comp_c)
    assert session.get_component_dependencies(comp_a) == [
        (comp_b, None),
        (comp_c, None),
    ]


def test_add_dependency_function():
    session = setup_session(deploy_manager=MockDeployManager(deploy_raise=True))
    comp_a = get_comp_type("a")(session=session)
    comp_b = get_comp_type("b")(session=session)
    verify_fn = lambda *_, **__: True
    comp_a.add_dependency(session, comp_b, verify_function=verify_fn)
    dep_list = session.get_component_dependencies(comp_a)
    assert len(dep_list) == 1
    assert dep_list[0][0] == comp_b
    assert dep_list[0][1].func == verify_fn


#############
## disable ##
#############


def test_disable_after_render():
    """Make sure disable runs successfully after rendering and correctly
    removes objects that are present.
    """
    deploy_manager = MockDeployManager(
        resources=[
            {
                "kind": "Foo",
                "metadata": {"name": "bar", "namespace": TEST_NAMESPACE},
            }
        ],
    )
    session = setup_session(deploy_manager=deploy_manager)
    comp = get_comp_type()(
        session=session,
        api_objects=[
            ("bar", {"kind": "Foo", "metadata": {"name": "bar"}}),
            ("bat", {"kind": "Baz", "metadata": {"name": "bat"}}),
        ],
    )
    comp.render_chart(session)
    assert deploy_manager.has_obj("Foo", "bar", TEST_NAMESPACE)
    assert not deploy_manager.has_obj("Baz", "bat", TEST_NAMESPACE)
    assert comp.disable(session)
    assert not deploy_manager.has_obj("Foo", "bar", TEST_NAMESPACE)
    assert not deploy_manager.has_obj("Baz", "bat", TEST_NAMESPACE)


def test_disable_before_render():
    """Make sure disable errors correctly if called before render"""
    session = setup_session()
    comp = get_comp_type()(session=session)
    with pytest.raises(AssertionError):
        comp.disable(session)


def test_disable_failure():
    """Make sure disable returns failure if the deploy_manager fails to run
    disable
    """
    session = setup_session(deploy_manager=MockDeployManager(disable_fail=True))
    comp = get_comp_type()(session=session)
    comp.render_chart(session)
    assert not comp.disable(session)


def test_disable_raise():
    """Make sure an exception thrown during disable correctly makes it out
    of the call
    """
    session = setup_session(deploy_manager=MockDeployManager(disable_raise=True))
    comp = get_comp_type()(session=session)
    comp.render_chart(session)
    with pytest.raises(AssertionError):
        comp.disable(session)


############
## verify ##
############


def test_verify_all_components_true():
    """Test that the default verify will use verify_resource to verify all
    managed objects and return True when they all verify successfully.
    """
    dm = MockDeployManager()
    with library_config(dry_run=False):
        session = setup_session(deploy_manager=dm)
        comp = get_comp_type()(
            session=session,
            api_objects=[
                ("bar", {"kind": "Foo", "metadata": {"name": "bar"}}),
                (
                    "bat",
                    {"kind": "Baz", "metadata": {"name": "bat", "namespace": "diff"}},
                ),
            ],
        )
        # Render and deploy
        comp.render_chart(session)
        comp.deploy(session)

        # Make sure components are verified because they exist
        assert comp.verify(session)


def test_verify_all_components_false():
    """Test that the default verify will use verify_resource to verify all
    managed objects and return False when they all are unverified.
    """
    dm = MockDeployManager(deploy_fail=True)
    with library_config(dry_run=False):
        session = setup_session(deploy_manager=dm)
        comp = get_comp_type()(
            session=session,
            api_objects=[
                ("bar", {"kind": "Foo", "metadata": {"name": "bar"}}),
                (
                    "bat",
                    {"kind": "Baz", "metadata": {"name": "bat", "namespace": "diff"}},
                ),
            ],
        )
        # Render and deploy
        comp.render_chart(session)
        comp.deploy(session)

        # Make sure resources are not verified because they didn't get deployed
        assert not comp.verify(session)


###########
## other ##
###########


def test_to_dict():
    """Make sure that the internal resource name is formatted correctly.
    This one is also pretty basic...
    """
    comp_name = "foo"
    session = setup_session()
    component = get_comp_type()(
        session=session,
        api_objects=[
            ("bar", {"kind": "Foo", "metadata": {"name": comp_name}}),
        ],
    )
    comp_dict = component.to_dict(session)

    assert comp_dict[0]["metadata"]["name"] == comp_name

    # Call it again to exercise the line that avoids re-calling render
    comp_dict2 = component.to_dict(session)
    assert comp_dict == comp_dict2


def test_to_config():
    """Make sure that the internal resource name is formatted correctly.
    This one is also pretty basic...
    """
    comp_name = "foo"
    session = setup_session()
    component = get_comp_type()(
        session=session,
        api_objects=[
            ("bar", {"kind": "Foo", "metadata": {"name": comp_name}}),
        ],
    )
    comp_config = component.to_config(session)

    assert comp_config[0].metadata.name == comp_name


def test_str():
    """Just make sure it doesn't raise!"""
    comp_name = "foo"
    component = get_comp_type(name=comp_name)(session=setup_session())
    assert comp_name in str(component)


def test_to_file():
    """Make sure that serializing to a file works cleanly.

    NOTE: At this point, file serialization is only intended to create files
        that can be subsequently applied outside of oper8, so we don't worry
        about safe round-tripping for things like dates and "on/off" style
        bools.
    """
    comp_name = "foo"
    component = get_comp_type()(
        session=setup_session(),
        api_objects=[
            ("bar", {"kind": "Foo", "metadata": {"name": comp_name}}),
        ],
    )
    with tempfile.TemporaryDirectory() as test_dir:
        with library_config(working_dir=test_dir):
            session = setup_session()
            comp_objects = component.to_dict(session)
            comp_file = component.to_file(session)
            assert os.path.exists(comp_file)
            with open(comp_file) as f:
                comp_objects2 = list(yaml.safe_load_all(f))
            assert comp_objects2 == comp_objects


def test_to_file_disabled():
    """Make sure that serializing to a file handles disabled components
    correctly by either not rendering them or rendering them in a special dir.
    """
    comp_name = "foo"
    component = get_comp_type()(
        session=setup_session(),
        api_objects=[
            ("bar", {"kind": "Foo", "metadata": {"name": comp_name}}),
        ],
        disabled=True,
    )

    with tempfile.TemporaryDirectory() as test_dir:
        # Test with dumping disabled
        with library_config(working_dir=test_dir, dump_disabled=False):
            session = setup_session()
            assert component.to_file(session) is None

        # Test with dumping enabled
        with library_config(working_dir=test_dir, dump_disabled=True):
            session = setup_session()
            comp_file = component.to_file(session)
            assert "DISABLED" in comp_file
            assert os.path.exists(comp_file)


def test_managed_object_repr():
    """Just make sure it doesn't raise!"""
    obj = ManagedObject(
        {
            "apiVersion": "v1",
            "kind": "Foo",
            "metadata": {"name": "foo", "namespace": "bar"},
        }
    )
    repr(obj)


def test_managed_object_eq():
    """Test that equality ignores definition"""
    obj_a = ManagedObject(
        {
            "apiVersion": "v1",
            "kind": "Foo",
            "metadata": {"name": "foo", "namespace": "bar"},
        }
    )
    obj_b = ManagedObject(
        {
            "apiVersion": "v1",
            "kind": "Foo",
            "metadata": {"name": "foo", "namespace": "bar"},
        }
    )
    obj_c = ManagedObject(
        {
            "apiVersion": "v1",
            "kind": "Foo",
            "metadata": {"name": "bar", "namespace": "bar"},
        }
    )
    obj_d = ManagedObject(
        {
            "apiVersion": "v1",
            "kind": "Baz",
            "metadata": {"name": "foo", "namespace": "bar"},
        }
    )
    obj_e = ManagedObject(
        {
            "apiVersion": "v2",
            "kind": "Foo",
            "metadata": {"name": "foo", "namespace": "bar"},
        }
    )
    assert obj_a == obj_b
    assert obj_a != obj_c
    assert obj_a != obj_d
    assert obj_a != obj_e
