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

import pytest
import asyncio
from datetime import datetime

# fmt: off
import logging

from everest.testing.core_utils.controller.test_controller_interface import TestController

from ocpp.routing import on, create_route_map
from ocpp.v201 import call as call201
from ocpp.v201 import call_result as call_result201
from ocpp.v201.enums import *
from ocpp.v201.datatypes import *
from everest.testing.ocpp_utils.fixtures import *
from everest_test_utils import * # Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one
from ocpp.v201.enums import (Action, IdTokenEnumType as IdTokenTypeEnum, SetVariableStatusEnumType, ClearCacheStatusEnumType, ConnectorStatusEnumType)
from validations import validate_status_notification_201
from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP2XConfigAdjustment, OCPP2XConfigVariableIdentifier
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, ValidationMode, validate_incoming_messages
# fmt: on

log = logging.getLogger("transactionsTest")


@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
async def test_E04(
    central_system_v201: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    """
    E04.FR.01
    ...
    """
    # prepare data for the test
    evse_id1 = 1
    connector_id = 1

    evse_id2 = 2

    # make an unknown IdToken
    id_token = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443)

    log.info(
        "##################### E04: Transaction started while charging station is offline #################"
    )

    test_controller.start()
    charge_point_v201 = await central_system_v201.wait_for_chargepoint(
        wait_for_bootnotification=True
    )

    # expect StatusNotification with status available
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "StatusNotification",
        call201.StatusNotification(
            datetime.now().isoformat(),
            ConnectorStatusEnumType.available,
            evse_id=evse_id1,
            connector_id=connector_id,
        ),
        validate_status_notification_201,
    )
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "StatusNotification",
        call201.StatusNotification(
            datetime.now().isoformat(),
            ConnectorStatusEnumType.available,
            evse_id=evse_id2,
            connector_id=connector_id,
        ),
        validate_status_notification_201,
    )

    # Enable AuthCacheCtrlr
    r: call_result201.SetVariables = (
        await charge_point_v201.set_config_variables_req(
            "AuthCacheCtrlr", "Enabled", "true"
        )
    )
    set_variable_result: SetVariableResultType = SetVariableResultType(
        **r.set_variable_result[0]
    )
    assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted

    # Enable LocalPreAuthorize
    r: call_result201.SetVariables = (
        await charge_point_v201.set_config_variables_req(
            "AuthCtrlr", "LocalPreAuthorize", "true"
        )
    )
    set_variable_result: SetVariableResultType = SetVariableResultType(
        **r.set_variable_result[0]
    )
    assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted

    # Set AuthCacheLifeTime
    r: call_result201.SetVariables = (
        await charge_point_v201.set_config_variables_req(
            "AuthCacheCtrlr", "LifeTime", "86400"
        )
    )
    set_variable_result: SetVariableResultType = SetVariableResultType(
        **r.set_variable_result[0]
    )
    assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted

    # Clear cache
    r: call_result201.ClearCache = await charge_point_v201.clear_cache_req()
    assert r.status == ClearCacheStatusEnumType.accepted

    # E04.FR.03 the queued transaction messages must contain the flag 'offline' as TRUE

    # Enable offline authorization for unknown ID
    r: call_result201.SetVariables = (
        await charge_point_v201.set_config_variables_req(
            "AuthCtrlr", "OfflineTxForUnknownIdEnabled", "true"
        )
    )
    set_variable_result: SetVariableResultType = SetVariableResultType(
        **r.set_variable_result[0]
    )
    assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted

    # Enable AlignedDataSignReadings (Not implemented yet)
    r: call_result201.SetVariables = (
        await charge_point_v201.set_config_variables_req(
            "AlignedDataCtrlr", "SignReadings", "true"
        )
    )
    set_variable_result: SetVariableResultType = SetVariableResultType(
        **r.set_variable_result[0]
    )
    assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted

    test_utility.messages.clear()

    # Disconnect CS
    log.debug(" Disconnect the CS from the CSMS")
    test_controller.disconnect_websocket()

    await asyncio.sleep(2)

    # swipe id tag to authorize
    test_controller.swipe(id_token.id_token)

    # start charging session
    test_controller.plug_in()

    # charge for 30 seconds
    await asyncio.sleep(30)

    # swipe id tag to de-authorize
    test_controller.swipe(id_token.id_token)

    # stop charging session
    test_controller.plug_out()

    await asyncio.sleep(10)

    # Connect CS
    log.debug(" Connect the CS to the CSMS")
    test_controller.connect_websocket()

    # wait for reconnect
    charge_point_v201 = await central_system_v201.wait_for_chargepoint(
        wait_for_bootnotification=False
    )

    # All offline generated transaction messaages must be marked offline = True

    # should send a  Transaction event C15.FR.02
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Started", "offline": True},
    )
    # should send a  Transaction event C15.FR.02
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Updated", "offline": True},
    )
    # should send a  Transaction event C15.FR.02
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Ended", "offline": True},
    )


@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.inject_csms_mock
@pytest.mark.ocpp_config_adaptions(
    GenericOCPP2XConfigAdjustment(
        [
            (
                OCPP2XConfigVariableIdentifier(
                    "OCPPCommCtrlr", "MessageTimeout", "Actual"
                ),
                "1",
            ),
            (
                OCPP2XConfigVariableIdentifier(
                    "OCPPCommCtrlr", "MessageAttemptInterval", "Actual"
                ),
                "1",
            ),
            (
                OCPP2XConfigVariableIdentifier(
                    "OCPPCommCtrlr", "MessageAttempts", "Actual"
                ),
                "3",
            ),
        ]
    )
)
@pytest.mark.flaky(reruns=1)
async def test_cleanup_transaction_events_after_max_attempts_exhausted(
    central_system: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    """
    Test if transaction events are properly cleaned up after the max message attempts
    ...
    """
    # prepare data for the test
    evse_id1 = 1
    connector_id = 1

    evse_id2 = 2
    connector_id2 = 1

    # make an unknown IdToken
    id_token = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443)

    test_controller.start()
    charge_point_v201 = await central_system.wait_for_chargepoint(
        wait_for_bootnotification=True
    )
    tx_event_attempt = SetVariableDataType(attribute_value="3", attribute_type=AttributeEnumType.actual,
                                           component=ComponentType(name="OCPPCommCtrlr"), variable=VariableType(name="MessageAttempts", instance="TransactionEvent"))
    tx_event_interval = SetVariableDataType(attribute_value="1", attribute_type=AttributeEnumType.actual,
                                            component=ComponentType(name="OCPPCommCtrlr"), variable=VariableType(name="MessageAttemptInterval", instance="TransactionEvent"))
    r: call_result201.SetVariables = (
        await charge_point_v201.set_variables_req(set_variable_data=[tx_event_attempt, tx_event_interval])
    )
    set_variable_result: SetVariableResultType = SetVariableResultType(
        **r.set_variable_result[0]
    )
    assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
    set_variable_result: SetVariableResultType = SetVariableResultType(
        **r.set_variable_result[1]
    )
    assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted

    # expect StatusNotification with status available
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "StatusNotification",
        call201.StatusNotification(
            datetime.now().isoformat(),
            ConnectorStatusEnumType.available,
            evse_id=evse_id1,
            connector_id=connector_id,
        ),
        validate_status_notification_201,
    )
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "StatusNotification",
        call201.StatusNotification(
            datetime.now().isoformat(),
            ConnectorStatusEnumType.available,
            evse_id=evse_id2,
            connector_id=connector_id2,
        ),
        validate_status_notification_201,
    )

    # return a CALLERROR for the transaction event
    central_system.mock.on_transaction_event.side_effect = [
        NotImplementedError()]

    # swipe id tag to authorize
    test_controller.swipe(id_token.id_token)

    # start charging session
    test_controller.plug_in()

    # should send a  Transaction event
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Started", "offline": False},
    )
    test_utility.validation_mode = ValidationMode.STRICT

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Started", "offline": False},
    )

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Started", "offline": False},
    )

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Updated", "offline": False},
    )

    test_utility.validation_mode = ValidationMode.EASY
    central_system.mock.on_transaction_event.reset()

    # respond properly to transaction events again
    central_system.mock.on_transaction_event.side_effect = [
        call_result201.TransactionEvent()
    ]

    # stop charging session
    test_controller.plug_out()

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Ended", "offline": False},
    )

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "StatusNotification",
        {"evseId": 1, "connectorId": 1, "connectorStatus": "Available"},
    )

    test_controller.stop()
    # add a sleep to allow time for reboot
    await asyncio.sleep(2)
    test_controller.start()

    # no attempts on delivering the transaction message should be made
    assert await validate_incoming_messages(test_utility, charge_point_v201, "TransactionEvent", {}, timeout=10) is False


@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.inject_csms_mock
@pytest.mark.ocpp_config_adaptions(
    GenericOCPP2XConfigAdjustment(
        [
            (
                OCPP2XConfigVariableIdentifier(
                    "AlignedDataCtrlr", "AlignedDataTxEndedInterval", "Actual"
                ),
                "5",
            )
        ]
    )
)
async def test_two_parallel_transactions(
    central_system: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    """
    Test if two parallel transactions work
    ...
    """
    # prepare data for the test
    evse_id1 = 1
    connector_id = 1

    evse_id2 = 2
    connector_id2 = 1

    # make an unknown IdToken
    id_token = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443)
    id_token2 = IdTokenType(id_token="ABAD1DEA", type=IdTokenTypeEnum.iso14443)

    test_controller.start()
    charge_point_v201 = await central_system.wait_for_chargepoint(
        wait_for_bootnotification=True
    )

    # expect StatusNotification with status available
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "StatusNotification",
        call201.StatusNotification(
            datetime.now().isoformat(),
            ConnectorStatusEnumType.available,
            evse_id=evse_id1,
            connector_id=connector_id,
        ),
        validate_status_notification_201,
    )
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "StatusNotification",
        call201.StatusNotification(
            datetime.now().isoformat(),
            ConnectorStatusEnumType.available,
            evse_id=evse_id2,
            connector_id=connector_id2,
        ),
        validate_status_notification_201,
    )

    # swipe id tag to authorize
    test_controller.swipe(id_token.id_token, connectors=[1])

    # start charging session
    test_controller.plug_in(evse_id1)

    # should send a  Transaction event
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Started", "offline": False},
    )

    # swipe id tag to authorize
    test_controller.swipe(id_token2.id_token, connectors=[2])

    # start charging session
    test_controller.plug_in(evse_id2)

    # should send a Transaction event
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Started", "offline": False},
    )
    # let transactions run for a bit
    await asyncio.sleep(10)
    # # stop charging session
    test_controller.plug_out(evse_id1)

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Ended", "offline": False},
    )
    test_controller.plug_out(evse_id2)
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Ended", "offline": False},
    )


@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
async def test_id_token_info_updated_in_tx_event(
    central_system_v201: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    # prepare data for the test
    evse_id1 = 1
    connector_id = 1

    evse_id2 = 2

    # make an unknown IdToken
    id_token = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443)

    log.info(
        "##################### Transaction with token info updated in transaction event #################"
    )

    test_controller.start()
    charge_point_v201 = await central_system_v201.wait_for_chargepoint(
        wait_for_bootnotification=True
    )

    # expect StatusNotification with status available
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "StatusNotification",
        call201.StatusNotification(
            datetime.now().isoformat(),
            ConnectorStatusEnumType.available,
            evse_id=evse_id1,
            connector_id=connector_id,
        ),
        validate_status_notification_201,
    )
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "StatusNotification",
        call201.StatusNotification(
            datetime.now().isoformat(),
            ConnectorStatusEnumType.available,
            evse_id=evse_id2,
            connector_id=connector_id,
        ),
        validate_status_notification_201,
    )

    @on(Action.transaction_event)
    def on_transaction_event(**kwargs):
        msg = call201.TransactionEvent(**kwargs)
        if msg.id_token != None:
            msg_token = IdTokenType(**msg.id_token)
            return call_result201.TransactionEvent(
                id_token_info=IdTokenInfoType(
                    status=AuthorizationStatusEnumType.accepted,
                    group_id_token=IdTokenType(
                        id_token="123", type=IdTokenTypeEnum.central
                    ),
                )
            )
        else:
            return call_result201.TransactionEvent()

    setattr(charge_point_v201, "on_transaction_event", on_transaction_event)
    central_system_v201.chargepoint.route_map = create_route_map(
        central_system_v201.chargepoint
    )
    # swipe id tag to authorize
    test_controller.swipe(id_token.id_token)
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "Authorize",
        {},
    )
    # start charging session
    test_controller.plug_in()
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Started"},
    )

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Updated"},
    )

    # charge for 10 seconds
    await asyncio.sleep(10)

    @on(Action.authorize)
    def on_authorize(**kwargs):
        msg = call201.Authorize(**kwargs)
        msg_token = IdTokenType(**msg.id_token)
        return call_result201.Authorize(
            id_token_info=IdTokenInfoType(
                status=AuthorizationStatusEnumType.accepted,
                group_id_token=IdTokenType(
                    id_token="123", type=IdTokenTypeEnum.central
                ),
            )
        )
    setattr(charge_point_v201, "on_authorize", on_authorize)
    central_system_v201.chargepoint.route_map = create_route_map(
        central_system_v201.chargepoint
    )
    # swipe id tag to de-authorize
    id_token = IdTokenType(id_token="8BADF00A", type=IdTokenTypeEnum.iso14443)
    test_controller.swipe(id_token.id_token)
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "Authorize",
        {},
    )
    # should send a  Transaction event C15.FR.02
    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Ended"},
    )

    # stop charging session
    test_controller.plug_out()

@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.everest_core_config('everest-config-ocpp201-ps.yaml')
@pytest.mark.use_temporary_persistent_store
@pytest.mark.ocpp_config_adaptions(
        GenericOCPP2XConfigAdjustment(
            [
                (
                    OCPP2XConfigVariableIdentifier(
                        "InternalCtrlr",
                        "ResumeTransactionsOnBoot",
                        "Actual",
                    ),
                    "true",
                ),
            ]
        )
    )
async def test_stop_pending_transactions(
    central_system_v201: CentralSystem,
    test_controller: TestController,
    test_utility: TestUtility,
):
    logging.info("######### test_stop_pending_transactions #########")

    # prepare data for the test
    evse_id = 1
    connector_id = 1
    remote_start_id = 1
    id_token = IdTokenType(id_token="DEADBEEF", type=IdTokenTypeEnum.iso14443)

    test_controller.start()
    charge_point_v201 = await central_system_v201.wait_for_chargepoint(
        wait_for_bootnotification=True
    )


    await charge_point_v201.request_start_transaction_req(
        id_token=id_token, remote_start_id=remote_start_id, evse_id=evse_id
    )

    test_controller.plug_in()

    assert await wait_for_and_validate(
        test_utility, charge_point_v201, "TransactionEvent", {
            "eventType": "Started"}
    )

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {"eventType": "Updated"},
    )

    # 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_v201 = await central_system_v201.wait_for_chargepoint(
        wait_for_bootnotification=False
    )

    await asyncio.sleep(2)

    assert await wait_for_and_validate(
        test_utility,
        charge_point_v201,
        "TransactionEvent",
        {
            "eventType": "Ended",
            "transactionInfo": {
                "stoppedReason": "PowerLoss"
            }
        },
    )