/* tslint:disable: no-string-literal */
import type { TestInterface } from "ava";
import anyTest, { ExecutionContext } from "ava";
import * as sinon from "sinon";
import { ImportMock } from "ts-mock-imports";
import { Delay } from "../../Src/Utils/Delay";
import JSONBigInt from "../../Src/Utils/JSONBigInt";
import { EPublishMessage, TPublishMessage, ZMQPublisher } from "../../Src/ZMQPublisher";
import { ERequestResponse, TRequestResponse, TSuccessfulRequest, ZMQRequest } from "../../Src/ZMQRequest";
import { ZMQResponse } from "../../Src/ZMQResponse";
import { TSubscriptionEndpoints, ZMQSubscriber } from "../../Src/ZMQSubscriber/ZMQSubscriber";
import { DUMMY_ENDPOINTS } from "../Helpers/DummyEndpoints.data";

type TTestContext =
{
    ResponderEndpoint: string;
    TestData: any[];
};

const test: TestInterface<TTestContext> = anyTest as TestInterface<TTestContext> ;

test.before((t: ExecutionContext<TTestContext>): void =>
{
    // Unnecessary
});

test.beforeEach((t: ExecutionContext<TTestContext>): void =>
{
    t.context = {
        ResponderEndpoint: "tcp://127.0.0.1:3241",
        TestData: [
            {
                a: 100n,
                b: 20n, // JSONBigInt will parse "20n" to 20n, known issue
                c: 0.5,
                d: [
                    5n,
                    "myFunc()",
                ],
            },
        ],
    };
});

test.afterEach((t: ExecutionContext<TTestContext>): void =>
{
    sinon.restore();
    ImportMock.restore();
});

test.serial("ZMQRequest: Start, Send, Receive, Close", async(t: ExecutionContext<TTestContext>): Promise<void> =>
{
    const lExpected: { code: string; data: any } =
    {
        code: "success",
        data: undefined!,
    };
    const lResponse: ZMQResponse = new ZMQResponse(t.context.ResponderEndpoint, async(aMsg: string): Promise<string> =>
    {
        let lResult: string;
        try
        {
            lResult = JSONBigInt.Parse(aMsg);
        }
        catch (e)
        {
            lResult = aMsg as string;
        }

        return JSONBigInt.Stringify({
            code: "success",
            data: lResult,
        });
    });

    const lRequester: ZMQRequest = new ZMQRequest(t.context.ResponderEndpoint);

    const lPromiseResult: TRequestResponse = await lRequester.Send(JSONBigInt.Stringify(t.context.TestData));
    lExpected.data = t.context.TestData;

    if (lPromiseResult.ResponseType === ERequestResponse.SUCCESS)
    {
        t.deepEqual(JSONBigInt.Parse(lPromiseResult.Response), lExpected);
    }
    else
    {
        t.fail(JSONBigInt.Stringify(
            {
                msg: "Request failed",
                lPromiseResult,
            },
        ));
    }

    const lNotThrowResult: TRequestResponse = await lRequester.Send("this should not throw");
    lExpected.data = "this should not throw";

    if (lNotThrowResult.ResponseType === ERequestResponse.SUCCESS)
    {
        t.deepEqual(JSONBigInt.Parse(lNotThrowResult.Response), lExpected);
    }
    else
    {
        t.fail(JSONBigInt.Stringify({msg: "Request failed", lNotThrowResult }));
    }

    lRequester.Close();
    lResponse.Close();
});

test.serial("ZMQResponse: Start, Receive, Close", async(t: ExecutionContext<TTestContext>): Promise<void> =>
{
    let lResponder = async(aMsg: string): Promise<string> => "world";
    const lResponderRouter = (aMsg: string): Promise<string> =>
    {
        return lResponder(aMsg);    // Necessary so we can update lResponder throughout
    };

    t.context.ResponderEndpoint = "tcp://127.0.0.1:4276";
    const lResponse: ZMQResponse = new ZMQResponse(t.context.ResponderEndpoint, lResponderRouter);
    const lRequester: ZMQRequest = new ZMQRequest(t.context.ResponderEndpoint);

    const lFirstResponse: TRequestResponse = await lRequester.Send("hello");

    t.is((lFirstResponse as TSuccessfulRequest).Response, "world");
    t.is(lResponse["mCachedRequests"].size, 1);

    lResponder = async(aMsg: string): Promise<string> => aMsg + " response";
    const lSecondResponse: TRequestResponse = await lRequester.Send("hello");

    t.is((lSecondResponse as TSuccessfulRequest).Response, "hello response");
    t.is(lResponse["mCachedRequests"].size, 2);

    lResponse.Close();
    lRequester.Close();
});

test.serial("ZMQPublisher & ZMQSubscriber", async(t: ExecutionContext<TTestContext>): Promise<void> =>
{
    const lStatusUpdatePublisher: ZMQPublisher = new ZMQPublisher(
        {
            PublisherAddress: DUMMY_ENDPOINTS.STATUS_UPDATES.PublisherAddress,
            RequestAddress: DUMMY_ENDPOINTS.STATUS_UPDATES.RequestAddress,
        },
    );
    const lWeatherUpdatePublisher: ZMQPublisher = new ZMQPublisher(
        {
            PublisherAddress: DUMMY_ENDPOINTS.WEATHER_UPDATES.PublisherAddress,
            RequestAddress: DUMMY_ENDPOINTS.WEATHER_UPDATES.RequestAddress,
        },
    );
    const lSubscriber: ZMQSubscriber = new ZMQSubscriber();

    await lStatusUpdatePublisher.Open();
    await lWeatherUpdatePublisher.Open();

    type TTestDataResult =
    {
        [index: string]: {
            Publisher: ZMQPublisher;
            Topics: {
                [index: string]: {
                    data: string[];
                    result: string[];
                };
            };
        };
    };

    const lTestDataResult: TTestDataResult =
    {
        [DUMMY_ENDPOINTS.STATUS_UPDATES.PublisherAddress]: {
            Publisher: lStatusUpdatePublisher,
            Topics: {},
        },
        [DUMMY_ENDPOINTS.WEATHER_UPDATES.PublisherAddress]: {
            Publisher: lWeatherUpdatePublisher,
            Topics: {},
        },
    };

    lTestDataResult[DUMMY_ENDPOINTS.STATUS_UPDATES.PublisherAddress].Topics["TopicA"]
        = { data: ["myTestMessage"], result: [] };
    lTestDataResult[DUMMY_ENDPOINTS.STATUS_UPDATES.PublisherAddress].Topics["TopicB"]
        = { data: ["myTestMessage"], result: [] };
    lTestDataResult[DUMMY_ENDPOINTS.STATUS_UPDATES.PublisherAddress].Topics["TopicC"]
        = { data: ["myTestMessage"], result: [] };
    lTestDataResult[DUMMY_ENDPOINTS.WEATHER_UPDATES.PublisherAddress].Topics["Sydney"]
        = { data: ["sunny"], result: [] };
    lTestDataResult[DUMMY_ENDPOINTS.WEATHER_UPDATES.PublisherAddress].Topics["Newcastle"]
        = { data: ["cloudy"], result: [] };

    const lSaveResult = (aEndpoint: string, aTopic: string, aNonce: number, aMessage: string): void =>
    {
        lTestDataResult[aEndpoint].Topics[aTopic].result[aNonce] = aMessage;
    };

    const lSubscribe = (aEndpoint: TSubscriptionEndpoints, aTopic: string): void =>
    {
        lSubscriber.Subscribe(aEndpoint, aTopic, (aMsg: string): void =>
        {
            lTestDataResult[aEndpoint.PublisherAddress].Publisher["mMessageCaches"].get(aTopic)!.forEach(
            (aValue: TPublishMessage, aKey: number): void =>
            {
                if (aValue[EPublishMessage.Message] === aMsg)
                {
                    lSaveResult(aEndpoint.PublisherAddress, aTopic, aKey, aMsg);
                }
            });
        });
    };

    lSubscribe({
        PublisherAddress: DUMMY_ENDPOINTS.STATUS_UPDATES.PublisherAddress,
        RequestAddress: DUMMY_ENDPOINTS.STATUS_UPDATES.RequestAddress,
    }, "TopicA");
    lSubscribe({
        PublisherAddress: DUMMY_ENDPOINTS.STATUS_UPDATES.PublisherAddress,
        RequestAddress: DUMMY_ENDPOINTS.STATUS_UPDATES.RequestAddress,
    }, "TopicB");
    lSubscribe({
        PublisherAddress: DUMMY_ENDPOINTS.STATUS_UPDATES.PublisherAddress,
        RequestAddress: DUMMY_ENDPOINTS.STATUS_UPDATES.RequestAddress,
    }, "TopicC");
    lSubscribe({
        PublisherAddress: DUMMY_ENDPOINTS.WEATHER_UPDATES.PublisherAddress,
        RequestAddress: DUMMY_ENDPOINTS.WEATHER_UPDATES.RequestAddress,
    }, "Newcastle");
    lSubscribe({
        PublisherAddress: DUMMY_ENDPOINTS.WEATHER_UPDATES.PublisherAddress,
        RequestAddress: DUMMY_ENDPOINTS.WEATHER_UPDATES.RequestAddress,
    }, "Sydney");

    for (const aEndpoint in lTestDataResult)
    {
        const lPublisher: ZMQPublisher = lTestDataResult[aEndpoint].Publisher;
        for (const aTopic in lTestDataResult[aEndpoint].Topics)
        {
            for (const aData of lTestDataResult[aEndpoint].Topics[aTopic].data)
            {
                lPublisher.Publish(aTopic, aData);
            }
        }
    }

    await Delay(50);

    for (const aEndpoint in lTestDataResult)
    {
        for (const aTopic in lTestDataResult[aEndpoint].Topics)
        {
            const lTestData: string[] = lTestDataResult[aEndpoint].Topics[aTopic].data;
            const lTestResult: string[] = lTestDataResult[aEndpoint].Topics[aTopic].result;
            for (let i: number = 0; i < lTestData.length; ++i)
            {
                t.is(lTestData[i], lTestResult[i]);
            }
        }
    }

    lSubscriber.Close();
    lStatusUpdatePublisher.Close();
    lWeatherUpdatePublisher.Close();
});

test.todo("Multiple Subscribers");
