/*
 * FreeRTOS Shadow V2.1.1
 * Copyright (C) 2020 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * http://aws.amazon.com/freertos
 * http://www.FreeRTOS.org
 */

/* Standard includes. */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* FreeRTOS includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

/* AWS includes. */
#include "aws_clientcredential.h"
#include "aws_shadow.h"
#include "iot_mqtt.h"
#include "iot_init.h"

/* Unity framework includes. */
#include "unity_fixture.h"
#include "unity.h"

#define shadowCLIENT_ID        clientcredentialIOT_THING_NAME
#define shadowTHING_NAME       clientcredentialIOT_THING_NAME
#define shadowTIMEOUT          pdMS_TO_TICKS( 10000UL )
/* Client token can be anything, as long as it's unique. */
#define shadowCLIENT_TOKEN     "aws-test-shadow-client-token"

#define shadowBUFFER_LENGTH    512
static char pcUpdateBuffer[ shadowBUFFER_LENGTH ];

/* Delay between test loops. */
#define shadowtestLOOP_DELAY    ( ( TickType_t ) 150 / portTICK_PERIOD_MS )

/* notification from callbacks to task*/
static SemaphoreHandle_t xShadowUpdateSemaphore;

/* Generate initial shadow document */
static uint32_t prvGenerateShadowJSON( void );

/* Called when shadow service received UPDATE request and updated thing shadow*/
BaseType_t prvTestUpdatedCallback( void * pvUserData,
                                   const char * const pcThingName,
                                   const char * const pcDeltaDocument,
                                   uint32_t ulDocumentLength,
                                   MQTTBufferHandle_t xBuffer );

/* Called when delta is reported in shadow document*/
BaseType_t prvTestDeltaCallback( void * pvUserData,
                                 const char * const pcThingName,
                                 const char * const pcDeltaDocument,
                                 uint32_t ulDocumentLength,
                                 MQTTBufferHandle_t xBuffer );

/* Called when shadow document is deleted*/
BaseType_t prvTestDeleteCallback( const char * const pcThingName,
                                  const char * const pcDeltaDocument,
                                  uint32_t ulDocumentLength,
                                  MQTTBufferHandle_t xBuffer );


TEST_GROUP( Full_Shadow );

TEST_SETUP( Full_Shadow )
{
    TEST_ASSERT_EQUAL( true, IotSdk_Init() );
    TEST_ASSERT_EQUAL( IOT_MQTT_SUCCESS, IotMqtt_Init() );
}

TEST_TEAR_DOWN( Full_Shadow )
{
    IotMqtt_Cleanup();
    IotSdk_Cleanup();
}

TEST_GROUP_RUNNER( Full_Shadow )
{
    RUN_TEST_CASE( Full_Shadow, ClientCreateDelete );
    RUN_TEST_CASE( Full_Shadow, ClientConnectDisconnect );
    RUN_TEST_CASE( Full_Shadow, CreateShadowDocument );
    RUN_TEST_CASE( Full_Shadow, DeleteShadowDocument );
    RUN_TEST_CASE( Full_Shadow, UpdateCallback );
}

/* Generate initial shadow document */
static uint32_t prvGenerateShadowJSON()
{
    /*simple shadow document for test */

    return ( uint32_t ) snprintf( pcUpdateBuffer, shadowBUFFER_LENGTH,
                                  "{"
                                  "\"state\":{"
                                  "\"desired\":{"
                                  "\"power\":\"on\""
                                  "},"
                                  "\"reported\":{"
                                  "\"power\":\"on\""
                                  "}"
                                  "},"
                                  "\"clientToken\": \"" shadowCLIENT_TOKEN "\""
                                                                           "}" );
}

/* Called when shadow service received UPDATE request and updated thing shadow*/
BaseType_t prvTestUpdatedCallback( void * pvUserData,
                                   const char * const pcThingName,
                                   const char * const pcDeltaDocument,
                                   uint32_t ulDocumentLength,
                                   MQTTBufferHandle_t xBuffer )
{
    ( void ) pvUserData;
    ( void ) pcThingName;
    ( void ) pcDeltaDocument;
    ( void ) ulDocumentLength;
    ( void ) xBuffer;

    /* notify testing task . */
    ( void ) xSemaphoreGive( xShadowUpdateSemaphore );

    return pdFALSE;
}

/* Called when delta is reported in shadow document*/
BaseType_t prvTestDeltaCallback( void * pvUserData,
                                 const char * const pcThingName,
                                 const char * const pcDeltaDocument,
                                 uint32_t ulDocumentLength,
                                 MQTTBufferHandle_t xBuffer )
{
    ( void ) pvUserData;
    ( void ) pcThingName;
    ( void ) pcDeltaDocument;
    ( void ) ulDocumentLength;
    ( void ) xBuffer;


    return pdFALSE;
}

/* Called when shadow document is deleted*/
BaseType_t prvTestDeleteCallback( const char * const pcThingName,
                                  const char * const pcDeltaDocument,
                                  uint32_t ulDocumentLength,
                                  MQTTBufferHandle_t xBuffer )
{
    ( void ) pcThingName;
    ( void ) pcDeltaDocument;
    ( void ) ulDocumentLength;
    ( void ) xBuffer;

    return pdFALSE;
}

/* helper functions for setting MQTT params. */
void TEST_SHADOW_Connect_Helper( MQTTAgentConnectParams_t * xConnectParams,
                                 ShadowClientHandle_t * pxShadowClientHandle )
{
    xConnectParams->pcURL = clientcredentialMQTT_BROKER_ENDPOINT;
    xConnectParams->xURLIsIPAddress = pdFALSE; /* Deprecated. */
    xConnectParams->usPort = clientcredentialMQTT_BROKER_PORT;

    xConnectParams->xSecuredConnection = pdFALSE; /* Deprecated. */
    xConnectParams->xFlags = mqttagentREQUIRE_TLS;
    xConnectParams->pcCertificate = NULL;
    xConnectParams->ulCertificateSize = 0;
    xConnectParams->pxCallback = NULL;
    xConnectParams->pvUserData = pxShadowClientHandle;

    xConnectParams->pucClientId = ( uint8_t * ) shadowCLIENT_ID;
    xConnectParams->usClientIdLength = ( uint16_t ) ( strlen( ( const char * ) shadowCLIENT_ID ) );
}

/* Test for shadow client create and delete api */
TEST( Full_Shadow, ClientCreateDelete )
{
    /*Init required params and shadow library for test.*/
    ShadowClientHandle_t xShadowClientHandle;
    ShadowCreateParams_t xCreateParams;
    ShadowReturnCode_t xReturn;
    uint32_t i, ulMaxLoopCount = 1;

    if( TEST_PROTECT() )
    {
        xCreateParams.xMQTTClientType = eDedicatedMQTTClient;

        for( i = 0; i < ulMaxLoopCount; i++ )
        {
            /*create the shadow client.*/
            xReturn = SHADOW_ClientCreate( &xShadowClientHandle, &xCreateParams );
            TEST_ASSERT_TRUE( eShadowSuccess == xReturn );

            /* Pause for a short while. */
            vTaskDelay( shadowtestLOOP_DELAY );

            /* if shadow client was created successfully test delete.*/
            xReturn = SHADOW_ClientDelete( xShadowClientHandle );

            TEST_ASSERT_TRUE( eShadowSuccess == xReturn );

            /* Pause for a short while. */
            vTaskDelay( shadowtestLOOP_DELAY );
        }
    }
    else
    {
        TEST_FAIL();
    }
}

/* Test for shadow client connect and disconnect api. */
TEST( Full_Shadow, ClientConnectDisconnect )
{
    /*Init required params and shadow library for test.*/
    ShadowClientHandle_t xShadowClientHandle;
    BaseType_t xClientCreated = pdFALSE;
    MQTTAgentConnectParams_t xConnectParams;
    ShadowCreateParams_t xCreateParams;
    ShadowReturnCode_t xReturn;
    uint32_t i, ulMaxLoopCount = 1;

    if( TEST_PROTECT() )
    {
        xCreateParams.xMQTTClientType = eDedicatedMQTTClient;

        /*create shadow client .*/
        xReturn = SHADOW_ClientCreate( &xShadowClientHandle, &xCreateParams );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
        xClientCreated = pdTRUE;

        /*set MQTT params.*/
        TEST_SHADOW_Connect_Helper( &xConnectParams, &xShadowClientHandle );

        for( i = 0; i < ulMaxLoopCount; i++ )
        {
            /*try connecting to shadow client.*/
            xReturn = SHADOW_ClientConnect( xShadowClientHandle,
                                            &xConnectParams,
                                            shadowTIMEOUT );
            TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );

            /* Pause for a short while to ensure the network is not too
             * congested. */
            vTaskDelay( shadowtestLOOP_DELAY );

            /*If connection is successful try to disconnect from shadow client.*/
            xReturn = SHADOW_ClientDisconnect( xShadowClientHandle );
            TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );

            /* Pause for a short while to ensure the network is not too
             * congested. */
            vTaskDelay( shadowtestLOOP_DELAY );
        }
    }
    else
    {
        TEST_FAIL();
    }

    if( xClientCreated )
    {
        /* delete shadow client before returning.*/
        xReturn = SHADOW_ClientDelete( xShadowClientHandle );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
    }
}

/* Test for shadow document is created/updated - SHADOW_Update api.*/
TEST( Full_Shadow, CreateShadowDocument )
{
    /*Init required params and shadow library for test.*/
    ShadowClientHandle_t xShadowClientHandle;
    BaseType_t xClientCreated = pdFALSE;
    MQTTAgentConnectParams_t xConnectParams;
    ShadowCreateParams_t xCreateParams;
    ShadowReturnCode_t xReturn;
    uint32_t ulCompareLength;
    ShadowOperationParams_t xOperationParams;

    if( TEST_PROTECT() )
    {
        xCreateParams.xMQTTClientType = eDedicatedMQTTClient;
        xReturn = SHADOW_ClientCreate( &xShadowClientHandle, &xCreateParams );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
        xClientCreated = pdTRUE;

        memset( &xConnectParams, 0x00, sizeof( xConnectParams ) );
        TEST_SHADOW_Connect_Helper( &xConnectParams, &xShadowClientHandle );
        xReturn = SHADOW_ClientConnect( xShadowClientHandle,
                                        &xConnectParams,
                                        shadowTIMEOUT );

        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );

        xOperationParams.pcThingName = shadowTHING_NAME;
        xOperationParams.xQoS = eMQTTQoS0;
        xOperationParams.pcData = NULL;
        /* subscribe to the accepted and rejected topics */
        xOperationParams.ucKeepSubscriptions = pdFALSE;
        xReturn = SHADOW_Delete( xShadowClientHandle,
                                 &xOperationParams,
                                 shadowTIMEOUT );
        TEST_ASSERT_TRUE( ( xReturn == eShadowSuccess ) || ( xReturn == eShadowRejectedNotFound ) );

        /*init shadow operations params. */
        xOperationParams.pcData = pcUpdateBuffer;
        xOperationParams.ulDataLength = ulCompareLength = prvGenerateShadowJSON();

        /* Only compare the state data when checking correctness. */
        ulCompareLength -= sizeof( "\"clientToken\": \"" shadowCLIENT_TOKEN "\"" );

        /* create the shadow document for thing.*/
        xReturn = SHADOW_Update( xShadowClientHandle,
                                 &xOperationParams,
                                 shadowTIMEOUT );

        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
        /* get the shadow document from shadow service.*/
        xReturn = SHADOW_Get( xShadowClientHandle,
                              &xOperationParams,
                              shadowTIMEOUT );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );

        if( memcmp( pcUpdateBuffer,
                    xOperationParams.pcData,
                    ulCompareLength ) != 0 )
        {
            TEST_FAIL();
        }

        /* Free the buffer used by shadow document. */
        xReturn = SHADOW_ReturnMQTTBuffer( xShadowClientHandle, xOperationParams.xBuffer );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );

        /* disconnect shadow client. */
        xReturn = SHADOW_ClientDisconnect( xShadowClientHandle );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
    }
    else
    {
        TEST_FAIL();
    }

    if( xClientCreated )
    {
        /* delete shadow client before returning.*/
        xReturn = SHADOW_ClientDelete( xShadowClientHandle );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
    }
}

/* Test for shadow document delete api.*/
TEST( Full_Shadow, DeleteShadowDocument )
{
    /*Init required params and shadow library for test.*/
    ShadowClientHandle_t xShadowClientHandle;
    BaseType_t xClientCreated = pdFALSE;
    MQTTAgentConnectParams_t xConnectParams;
    ShadowCreateParams_t xCreateParams;
    ShadowReturnCode_t xReturn;
    ShadowOperationParams_t xOperationParams;

    if( TEST_PROTECT() )
    {
        xCreateParams.xMQTTClientType = eDedicatedMQTTClient;
        xReturn = SHADOW_ClientCreate( &xShadowClientHandle, &xCreateParams );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
        xClientCreated = pdTRUE;

        memset( &xConnectParams, 0x00, sizeof( xConnectParams ) );
        TEST_SHADOW_Connect_Helper( &xConnectParams, &xShadowClientHandle );
        xReturn = SHADOW_ClientConnect( xShadowClientHandle,
                                        &xConnectParams,
                                        shadowTIMEOUT );

        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );

        xOperationParams.pcThingName = shadowTHING_NAME;
        xOperationParams.xQoS = eMQTTQoS0;
        xOperationParams.pcData = pcUpdateBuffer;
        /* subscribe to the accepted and rejected topics */
        xOperationParams.ucKeepSubscriptions = pdFALSE;

        /* generate a simple json document for test. */
        xOperationParams.ulDataLength = prvGenerateShadowJSON();

        /* create the shadow document for thing.*/
        xReturn = SHADOW_Update( xShadowClientHandle,
                                 &xOperationParams,
                                 shadowTIMEOUT );

        /*Delete the shadow document just created. */
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
        /* delete the shadow document is same .*/
        xReturn = SHADOW_Delete( xShadowClientHandle,
                                 &xOperationParams,
                                 shadowTIMEOUT );


        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
        /* try to get the deleted document .*/
        xReturn = SHADOW_Get( xShadowClientHandle,
                              &xOperationParams,
                              shadowTIMEOUT );

        TEST_ASSERT_TRUE( xOperationParams.pcData == NULL )


        /*disconnect shadow client*/
        xReturn = SHADOW_ClientDisconnect( xShadowClientHandle );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
    }
    else
    {
        TEST_FAIL();
    }

    if( xClientCreated )
    {
        /* delete shadow client before returning.*/
        xReturn = SHADOW_ClientDelete( xShadowClientHandle );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
    }
}

/* Test for shadow document update callback.*/
TEST( Full_Shadow, UpdateCallback )
{
    /*Init required params and shadow library for test.*/
    ShadowClientHandle_t xShadowClientHandle;
    BaseType_t xClientCreated = pdFALSE;
    BaseType_t xSemaphoreCreated = pdFALSE;
    MQTTAgentConnectParams_t xConnectParams;
    ShadowCallbackParams_t xCallbackParams;
    ShadowCreateParams_t xCreateParams;
    ShadowReturnCode_t xReturn;

    if( TEST_PROTECT() )
    {
        xCreateParams.xMQTTClientType = eDedicatedMQTTClient;
        xShadowUpdateSemaphore = xSemaphoreCreateBinary();
        TEST_ASSERT_TRUE( xShadowUpdateSemaphore != NULL );
        xSemaphoreCreated = pdTRUE;

        xReturn = SHADOW_ClientCreate( &xShadowClientHandle, &xCreateParams );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
        xClientCreated = pdTRUE;

        memset( &xConnectParams, 0x00, sizeof( xConnectParams ) );
        TEST_SHADOW_Connect_Helper( &xConnectParams, &xShadowClientHandle );
        xReturn = SHADOW_ClientConnect( xShadowClientHandle,
                                        &xConnectParams,
                                        shadowTIMEOUT );

        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
        /*Register update callback.*/
        xCallbackParams.pcThingName = shadowTHING_NAME;
        xCallbackParams.xShadowUpdatedCallback = prvTestUpdatedCallback;
        xCallbackParams.xShadowDeltaCallback = prvTestDeltaCallback;
        xReturn = SHADOW_RegisterCallbacks( xShadowClientHandle,
                                            &xCallbackParams,
                                            shadowTIMEOUT );

        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
        ShadowOperationParams_t xOperationParams;
        xOperationParams.pcThingName = shadowTHING_NAME;
        xOperationParams.xQoS = eMQTTQoS0;
        xOperationParams.pcData = pcUpdateBuffer;
        /* subscribe to the accepted and rejected topics */
        xOperationParams.ucKeepSubscriptions = pdFALSE;

        /* generate a simple json document for test. */
        xOperationParams.ulDataLength = prvGenerateShadowJSON();

        /* create the shadow document for thing.*/
        xReturn = SHADOW_Update( xShadowClientHandle,
                                 &xOperationParams,
                                 shadowTIMEOUT );


        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );

        /*wait for update callback.*/


        /* See if we can obtain the semaphore.  If the semaphore is not
         * available wait 10 ticks to see if it becomes free. */
        vTaskDelay( ( TickType_t ) 10 );

        if( xSemaphoreTake( xShadowUpdateSemaphore, ( TickType_t ) 10 ) == pdFALSE )
        {
            /* We were not able to obtain the semaphore. */
            TEST_FAIL();
        }

        xReturn = SHADOW_ClientDisconnect( xShadowClientHandle );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
    }
    else
    {
        TEST_FAIL();
    }

    if( xClientCreated )
    {
        /* delete shadow client before returning.*/
        xReturn = SHADOW_ClientDelete( xShadowClientHandle );
        TEST_ASSERT_EQUAL( eShadowSuccess, xReturn );
    }

    if( xSemaphoreCreated )
    {
        vSemaphoreDelete( xShadowUpdateSemaphore );
    }
}
