# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest

from everest.testing.core_utils.controller.test_controller_interface import (
    TestController,
)
# fmt: off
from ocpp.routing import create_route_map, on
from ocpp.v16.enums import *
from ocpp.v16 import call
from datetime import datetime, timezone
import asyncio
import logging
import pytest
from validations import (validate_standard_start_transaction,
                               validate_standard_stop_transaction,
                               validate_boot_notification
                               )
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, OcppTestConfiguration
from everest.testing.ocpp_utils.fixtures import charge_point_v16
from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16
from everest.testing.ocpp_utils.central_system import CentralSystem
from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP16ConfigAdjustment
from everest_test_utils import *
# fmt: on


@pytest.mark.asyncio
async def test_stop_pending_transactions(
    test_config: OcppTestConfiguration,
    charge_point_v16: ChargePoint16,
    test_utility: TestUtility,
    test_controller: TestController,
    central_system_v16: CentralSystem,
):
    logging.info("######### test_stop_pending_transactions #########")

    # start charging session
    test_controller.plug_in()

    # send RemoteStartTransaction.req
    await charge_point_v16.remote_start_transaction_req(
        id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1
    )

    # expect StartTransaction.req
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "StartTransaction",
        call.StartTransaction(
            1, test_config.authorization_info.valid_id_tag_1, 0, ""
        ),
        validate_standard_start_transaction,
    )

    # expect StatusNotification with status charging
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "StatusNotification",
        call.StatusNotification(
            1, ChargePointErrorCode.no_error, ChargePointStatus.charging
        ),
    )

    # charge for some time...
    logging.debug("Charging for a while...")
    await asyncio.sleep(2)

    test_controller.stop()

    await asyncio.sleep(2)

    test_controller.start()

    charge_point_v16 = await central_system_v16.wait_for_chargepoint(
        wait_for_bootnotification=False
    )

    await asyncio.sleep(2)

    # expect StopTransaction.req
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "StopTransaction",
        call.StopTransaction(0, "", 1, Reason.power_loss),
        validate_standard_stop_transaction,
    )


@pytest.mark.everest_core_config(
    get_everest_config_path_str("everest-config-security-profile-1.yaml")
)
@pytest.mark.asyncio
async def test_change_authorization_key_in_pending(
    test_config: OcppTestConfiguration,
    central_system_v16: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    logging.info(
        "######### test_change_authorization_key_in_pending #########")

    @on(Action.boot_notification)
    def on_boot_notification_pending(**kwargs):
        return call_result.BootNotification(
            current_time=datetime.now(timezone.utc).isoformat(),
            interval=10,
            status=RegistrationStatus.pending,
        )

    @on(Action.boot_notification)
    def on_boot_notification_accepted(**kwargs):
        return call_result.BootNotification(
            current_time=datetime.now(timezone.utc).isoformat(),
            interval=5,
            status=RegistrationStatus.accepted,
        )

    central_system_v16.function_overrides.append(
        ("on_boot_notification", on_boot_notification_pending)
    )

    test_controller.start()
    charge_point_v16 = await central_system_v16.wait_for_chargepoint()
    charge_point_v16.pipe = True

    response = await charge_point_v16.get_configuration_req()
    assert len(response.configuration_key) > 20

    await charge_point_v16.change_configuration_req(
        key="MeterValueSampleInterval", value="10"
    )
    await charge_point_v16.change_configuration_req(
        key="AuthorizationKey", value="DEADBEEFDEADBEEF"
    )

    # wait for reconnect
    await central_system_v16.wait_for_chargepoint(wait_for_bootnotification=False)
    charge_point_v16 = central_system_v16.chargepoint

    setattr(charge_point_v16, "on_boot_notification",
            on_boot_notification_accepted)
    central_system_v16.chargepoint.route_map = create_route_map(
        central_system_v16.chargepoint
    )

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "BootNotification",
        call.BootNotification(
            test_config.charge_point_info.charge_point_model,
            charge_box_serial_number=test_config.charge_point_info.charge_point_id,
            charge_point_vendor=test_config.charge_point_info.charge_point_vendor,
            firmware_version=test_config.charge_point_info.firmware_version,
        ),
        validate_boot_notification,
    )

    # expect StatusNotification.req with status available
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "StatusNotification",
        call.StatusNotification(
            1, ChargePointErrorCode.no_error, ChargePointStatus.available
        ),
    )

    assert await wait_for_and_validate(
        test_utility, charge_point_v16, "Heartbeat", call.Heartbeat()
    )


@pytest.mark.everest_core_config(
    get_everest_config_path_str("everest-config-security-profile-1.yaml")
)
@pytest.mark.asyncio
async def test_remote_start_stop_in_pending(
    test_config: OcppTestConfiguration,
    central_system_v16: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    logging.info(
        "######### test_change_authorization_key_in_pending #########")

    @on(Action.boot_notification)
    def on_boot_notification_pending(**kwargs):
        return call_result.BootNotification(
            current_time=datetime.now(timezone.utc).isoformat(),
            interval=10,
            status=RegistrationStatus.pending,
        )

    central_system_v16.function_overrides.append(
        ("on_boot_notification", on_boot_notification_pending)
    )

    test_controller.start()
    charge_point_v16 = await central_system_v16.wait_for_chargepoint()
    charge_point_v16.pipe = True

    await charge_point_v16.remote_start_transaction_req(id_tag="DEADBEEF")
    assert await wait_for_and_validate(
        test_utility, charge_point_v16, "RemoteStartTransaction", {
            "status": "Rejected"}
    )

    await charge_point_v16.remote_stop_transaction_req(transaction_id=20)
    assert await wait_for_and_validate(
        test_utility, charge_point_v16, "RemoteStopTransaction", {
            "status": "Rejected"}
    )


@pytest.mark.asyncio
async def test_boot_notification_rejected(
    test_config: OcppTestConfiguration,
    central_system_v16: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    logging.info("######### test_boot_notification_rejected #########")

    @on(Action.boot_notification)
    def on_boot_notification_rejected(**kwargs):
        return call_result.BootNotification(
            current_time=datetime.now(timezone.utc).isoformat(),
            interval=10,
            status=RegistrationStatus.rejected,
        )

    @on(Action.boot_notification)
    def on_boot_notification_accepted(**kwargs):
        return call_result.BootNotification(
            current_time=datetime.now(timezone.utc).isoformat(),
            interval=5,
            status=RegistrationStatus.accepted,
        )

    central_system_v16.function_overrides.append(
        ("on_boot_notification", on_boot_notification_rejected)
    )

    test_controller.start()
    charge_point_v16: ChargePoint16 = await central_system_v16.wait_for_chargepoint()
    charge_point_v16.pipe = True

    setattr(charge_point_v16, "on_boot_notification",
            on_boot_notification_accepted)
    central_system_v16.chargepoint.route_map = create_route_map(
        central_system_v16.chargepoint
    )

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "BootNotification",
        call.BootNotification(
            test_config.charge_point_info.charge_point_model,
            charge_box_serial_number=test_config.charge_point_info.charge_point_id,
            charge_point_vendor=test_config.charge_point_info.charge_point_vendor,
            firmware_version=test_config.charge_point_info.firmware_version,
        ),
        validate_boot_notification,
    )

    # expect StatusNotification.req with status available
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "StatusNotification",
        call.StatusNotification(
            1, ChargePointErrorCode.no_error, ChargePointStatus.available
        ),
    )

    assert await wait_for_and_validate(
        test_utility, charge_point_v16, "Heartbeat", call.Heartbeat()
    )


@pytest.mark.asyncio
async def test_boot_notification_callerror(
    test_config: OcppTestConfiguration,
    central_system_v16: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    logging.info("######### test_boot_notification_callerror #########")

    @on(Action.boot_notification)
    def on_boot_notification_accepted(**kwargs):
        return call_result.BootNotification(
            current_time=datetime.now(timezone.utc).isoformat(),
            interval=5,
            status=RegistrationStatus.accepted,
        )

    # Provoke a CALLERROR as a response to a BootNotification.req
    central_system_v16.function_overrides.append(
        ("on_boot_notification", None))

    test_controller.start()
    charge_point_v16: ChargePoint16 = await central_system_v16.wait_for_chargepoint()
    charge_point_v16.pipe = True

    setattr(charge_point_v16, "on_boot_notification",
            on_boot_notification_accepted)
    central_system_v16.chargepoint.route_map = create_route_map(
        central_system_v16.chargepoint
    )

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "BootNotification",
        call.BootNotification(
            test_config.charge_point_info.charge_point_model,
            charge_box_serial_number=test_config.charge_point_info.charge_point_id,
            charge_point_vendor=test_config.charge_point_info.charge_point_vendor,
            firmware_version=test_config.charge_point_info.firmware_version,
        ),
        validate_boot_notification,
        timeout=100,
    )

    # expect StatusNotification.req with status available
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "StatusNotification",
        call.StatusNotification(
            1, ChargePointErrorCode.no_error, ChargePointStatus.available
        ),
    )

    assert await wait_for_and_validate(
        test_utility, charge_point_v16, "Heartbeat", call.Heartbeat()
    )


@pytest.mark.asyncio
async def test_boot_notification_no_response(
    test_config: OcppTestConfiguration,
    central_system_v16: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    logging.info("######### test_boot_notification_no_response #########")

    async def route_message(msg):
        return

    # do not respond at all
    central_system_v16.function_overrides.append(
        ("route_message", route_message))

    test_controller.start()
    charge_point_v16: ChargePoint16 = await central_system_v16.wait_for_chargepoint()
    charge_point_v16.pipe = True

    # this is the second BootNotification.req
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "BootNotification",
        call.BootNotification(
            test_config.charge_point_info.charge_point_model,
            charge_box_serial_number=test_config.charge_point_info.charge_point_id,
            charge_point_vendor=test_config.charge_point_info.charge_point_vendor,
            firmware_version=test_config.charge_point_info.firmware_version,
        ),
        validate_boot_notification,
        timeout=100,
    )


@pytest.mark.asyncio
@pytest.mark.everest_core_config(
    get_everest_config_path_str("everest-config-security-profile-2.yaml")
)
@pytest.mark.source_certs_dir(Path(__file__).parent / "../everest-aux/certs")
@pytest.mark.asyncio
@pytest.mark.csms_tls
@pytest.mark.ocpp_config_adaptions(
    GenericOCPP16ConfigAdjustment(
        [("Internal", "VerifyCsmsCommonName", False)])
)
async def test_initiate_message_in_pending(
    test_config: OcppTestConfiguration,
    central_system_v16: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    logging.info("######### test_initiate_message_in_pending #########")

    @on(Action.boot_notification)
    def on_boot_notification_pending(**kwargs):
        return call_result.BootNotification(
            current_time=datetime.now(timezone.utc).isoformat(),
            interval=10,
            status=RegistrationStatus.pending,
        )

    @on(Action.boot_notification)
    def on_boot_notification_accepted(**kwargs):
        return call_result.BootNotification(
            current_time=datetime.now(timezone.utc).isoformat(),
            interval=5,
            status=RegistrationStatus.accepted,
        )

    central_system_v16.function_overrides.append(
        ("on_boot_notification", on_boot_notification_pending)
    )

    test_utility.forbidden_actions.append("SecurityEventNotification")

    test_controller.start()
    charge_point_v16: ChargePoint16 = await central_system_v16.wait_for_chargepoint()
    charge_point_v16.pipe = True

    await charge_point_v16.change_configuration_req(key="CpoName", value="VENID")

    await charge_point_v16.extended_trigger_message_req(
        requested_message=MessageTrigger.status_notification
    )
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "StatusNotification",
        call.StatusNotification(
            1, ChargePointErrorCode.no_error, ChargePointStatus.available
        ),
    )

    test_utility.messages.clear()
    await charge_point_v16.extended_trigger_message_req(
        requested_message=MessageTrigger.boot_notification
    )
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "BootNotification",
        call.BootNotification(
            test_config.charge_point_info.charge_point_model,
            charge_box_serial_number=test_config.charge_point_info.charge_point_id,
            charge_point_vendor=test_config.charge_point_info.charge_point_vendor,
            firmware_version=test_config.charge_point_info.firmware_version,
        ),
        validate_boot_notification,
    )

    test_utility.messages.clear()
    await charge_point_v16.extended_trigger_message_req(
        requested_message=MessageTrigger.heartbeat
    )
    assert await wait_for_and_validate(
        test_utility, charge_point_v16, "Heartbeat", call.Heartbeat()
    )

    test_utility.messages.clear()
    await charge_point_v16.trigger_message_req(
        requested_message=MessageTrigger.diagnostics_status_notification
    )
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "DiagnosticsStatusNotification",
        call.DiagnosticsStatusNotification(DiagnosticsStatus.idle),
    )

    test_utility.messages.clear()
    await charge_point_v16.trigger_message_req(
        requested_message=MessageTrigger.firmware_status_notification
    )
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "FirmwareStatusNotification",
        call.FirmwareStatusNotification(FirmwareStatus.idle),
    )

    test_utility.messages.clear()
    await charge_point_v16.trigger_message_req(
        requested_message=MessageTrigger.status_notification
    )
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "StatusNotification",
        call.StatusNotification(
            1, ChargePointErrorCode.no_error, ChargePointStatus.available
        ),
    )

    await charge_point_v16.extended_trigger_message_req(
        requested_message=MessageTrigger.sign_charge_point_certificate
    )
    # expect ExtendedTriggerMessage.conf with status Accepted
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "ExtendedTriggerMessage",
        call_result.ExtendedTriggerMessage(TriggerMessageStatus.accepted),
    )

    assert await wait_for_and_validate(
        test_utility, charge_point_v16, "SignCertificate", {}
    )

    setattr(charge_point_v16, "on_boot_notification",
            on_boot_notification_accepted)
    central_system_v16.chargepoint.route_map = create_route_map(
        central_system_v16.chargepoint
    )

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "BootNotification",
        call.BootNotification(
            test_config.charge_point_info.charge_point_model,
            charge_box_serial_number=test_config.charge_point_info.charge_point_id,
            charge_point_vendor=test_config.charge_point_info.charge_point_vendor,
            firmware_version=test_config.charge_point_info.firmware_version,
        ),
        validate_boot_notification,
    )

    test_utility.forbidden_actions.clear()
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "SecurityEventNotification",
        {"type": "StartupOfTheDevice"},
    )

    # expect StatusNotification.req with status available
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v16,
        "StatusNotification",
        call.StatusNotification(
            1, ChargePointErrorCode.no_error, ChargePointStatus.available
        ),
    )

    assert await wait_for_and_validate(
        test_utility, charge_point_v16, "Heartbeat", call.Heartbeat()
    )


@pytest.mark.asyncio
async def test_boot_notification_rejected_and_call_by_csms(
    test_config: OcppTestConfiguration,
    central_system_v16: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    @on(Action.boot_notification)
    def on_boot_notification_rejected(**kwargs):
        return call_result.BootNotification(
            current_time=datetime.now(timezone.utc).isoformat(),
            interval=10,
            status=RegistrationStatus.rejected,
        )

    central_system_v16.function_overrides.append(
        ("on_boot_notification", on_boot_notification_rejected)
    )

    test_controller.start()
    charge_point_v16: ChargePoint16 = await central_system_v16.wait_for_chargepoint()
    charge_point_v16.pipe = True

    # Response to this message is not allowed
    test_utility.forbidden_actions.append("RemoteStartTransaction")

    t = threading.Thread(
        target=asyncio.run,
        args=(
            charge_point_v16.remote_start_transaction_req(
                id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1
            ),
        ),
    )
    t.start()

    assert await wait_for_and_validate(
        test_utility, charge_point_v16, "BootNotification", {}
    )
