//
// Copyright (c) ZeroC, Inc. All rights reserved.
//

(function(module, require, exports)
{
    const Ice = require("ice").Ice;
    const Test = require("Test").Test;
    const TestHelper = require("TestHelper").TestHelper;
    const test = TestHelper.test;

    class Client extends TestHelper
    {
        async allTests()
        {
            class PreservedI extends Test.Preserved
            {
                constructor()
                {
                    super();
                    ++PreservedI.counter;
                }
            }

            function PreservedFactoryI(id)
            {
                return id === Test.Preserved.ice_staticId() ? new PreservedI() : null;
            }

            const out = this.getWriter();
            const communicator = this.communicator();

            out.write("testing stringToProxy... ");
            const ref = "Test:" + this.getTestEndpoint() + " -t 10000";
            const base = communicator.stringToProxy(ref);
            test(base !== null);
            out.writeLine("ok");

            out.write("testing checked cast... ");
            const prx = await Test.TestIntfPrx.checkedCast(base);
            test(prx !== null);
            test(prx.equals(base));
            out.writeLine("ok");

            out.write("base as Object... ");
            {
                const sb = await prx.SBaseAsObject();
                test(sb !== null);
                test(sb.ice_id() == "::Test::SBase");
                test(sb.sb == "SBase.sb");
            }
            out.writeLine("ok");

            out.write("base as base... ");
            {
                const sb = await prx.SBaseAsSBase();
                test(sb.sb == "SBase.sb");
            }
            out.writeLine("ok");

            out.write("base with known derived as base... ");
            {
                const sb = await prx.SBSKnownDerivedAsSBase();
                test(sb !== null);
                test(sb.sb == "SBSKnownDerived.sb");
                test(sb.sbskd == "SBSKnownDerived.sbskd");
            }
            out.writeLine("ok");

            out.write("base with known derived as known derived... ");
            {
                const sb = await prx.SBSKnownDerivedAsSBSKnownDerived();
                test(sb.sbskd == "SBSKnownDerived.sbskd");
            }
            out.writeLine("ok");

            out.write("base with unknown derived as base... ");
            {
                const sb = await prx.SBSUnknownDerivedAsSBase();
                test(sb.sb == "SBSUnknownDerived.sb");
            }

            if(prx.ice_getEncodingVersion().equals(Ice.Encoding_1_0))
            {
                try
                {
                    const sb = await prx.SBSUnknownDerivedAsSBaseCompact();
                    test(sb.sb == "SBSUnknownDerived.sb");
                }
                catch(ex)
                {
                    test(ex instanceof Ice.OperationNotExistException, ex);
                }
            }
            else
            {
                try
                {
                    await prx.SBSUnknownDerivedAsSBaseCompact();
                    test(false);
                }
                catch(ex)
                {
                    test(ex instanceof Ice.OperationNotExistException ||
                         ex instanceof Ice.NoValueFactoryException, ex);
                }
            }
            out.writeLine("ok");

            out.write("unknown with Object as Object... ");
            try
            {
                const obj = await prx.SUnknownAsObject();
                test(!prx.ice_getEncodingVersion().equals(Ice.Encoding_1_0));
                test(obj instanceof Ice.UnknownSlicedValue);
                test(obj.ice_id() == "::Test::SUnknown");
                test(obj.ice_getSlicedData() !== null);
                await prx.checkSUnknown(obj);
            }
            catch(ex)
            {
                test(ex instanceof Ice.NoValueFactoryException, ex);
                test(prx.ice_getEncodingVersion().equals(Ice.Encoding_1_0));
            }
            out.writeLine("ok");

            out.write("one-element cycle... ");
            {
                const b = await prx.oneElementCycle();
                test(b !== null);
                test(b.ice_id() == "::Test::B");
                test(b.sb == "B1.sb");
                test(b.pb === b);
            }
            out.writeLine("ok");

            out.write("two-element cycle... ");
            {
                const b1 = await prx.twoElementCycle();
                test(b1 !== null);
                test(b1.ice_id() == "::Test::B");
                test(b1.sb == "B1.sb");

                const b2 = b1.pb;
                test(b2 !== null);
                test(b2.ice_id() == "::Test::B");
                test(b2.sb == "B2.sb");
                test(b2.pb == b1);
            }
            out.writeLine("ok");

            out.write("known derived pointer slicing as derived... ");
            {
                const d1 = await prx.D1AsD1();

                test(d1 !== null);
                test(d1.ice_id() == "::Test::D1");
                test(d1.sb == "D1.sb");
                test(d1.pb !== null);
                test(d1.pb !== d1);

                const b2 = d1.pb;
                test(b2 !== null);
                test(b2.ice_id() == "::Test::B");
                test(b2.sb == "D2.sb");
                test(b2.pb === d1);
            }
            out.writeLine("ok");

            out.write("unknown derived pointer slicing as base... ");
            {
                const b2 = await prx.D2AsB();

                test(b2 !== null);
                test(b2.ice_id() == "::Test::B");
                test(b2.sb == "D2.sb");
                test(b2.pb !== null);
                test(b2.pb !== b2);

                const b1 = b2.pb;
                test(b1 !== null);
                test(b1.ice_id() == "::Test::D1");
                test(b1.sb == "D1.sb");
                test(b1.pb == b2);
                test(b1.sd1 == "D1.sd1");
                test(b1.pd1 === b2);
            }
            out.writeLine("ok");

            out.write("param ptr slicing with known first... ");
            {
                const [b1, b2] = await prx.paramTest1();
                test(b1 !== null);
                test(b1.ice_id() == "::Test::D1");
                test(b1.sb == "D1.sb");
                test(b1.pb == b2);
                test(b1 !== null);
                test(b1.sd1 == "D1.sd1");
                test(b1.pd1 === b2);

                test(b2 !== null);
                test(b2.ice_id() == "::Test::B"); // No factory, must be sliced
                test(b2.sb == "D2.sb");
                test(b2.pb === b1);
            }
            out.writeLine("ok");

            out.write("param ptr slicing with unknown first... ");
            {
                const [b2, b1] = await prx.paramTest2();
                test(b1 !== null);
                test(b1.ice_id() == "::Test::D1");
                test(b1.sb == "D1.sb");
                test(b1.pb === b2);
                test(b1 !== null);
                test(b1.sd1 == "D1.sd1");
                test(b1.pd1 == b2);

                test(b2 !== null);
                test(b2.ice_id() == "::Test::B"); // No factory, must be sliced
                test(b2.sb == "D2.sb");
                test(b2.pb == b1);
            }
            out.writeLine("ok");

            out.write("return value identity with known first... ");
            {
                const [ret, p1] = await prx.returnTest1();
                test(ret === p1);
            }
            out.writeLine("ok");

            out.write("return value identity with unknown first... ");
            {
                const [ret, p1] = await prx.returnTest2();
                test(ret == p1);
            }
            out.writeLine("ok");

            out.write("return value identity for input params known first... ");
            {
                const d1 = new Test.D1();
                d1.sb = "D1.sb";
                d1.sd1 = "D1.sd1";
                const d3 = new Test.D3();
                d3.pb = d1;
                d3.sb = "D3.sb";
                d3.sd3 = "D3.sd3";
                d3.pd3 = d1;
                d1.pb = d3;
                d1.pd1 = d3;

                const b1 = await prx.returnTest3(d1, d3);
                test(b1 !== null);
                test(b1.sb == "D1.sb");
                test(b1.ice_id() == "::Test::D1");
                const p1 = b1;
                test(p1 !== null);
                test(p1.sd1 == "D1.sd1");
                test(p1.pd1 == b1.pb);

                const b2 = b1.pb;
                test(b2 !== null);
                test(b2.sb == "D3.sb");
                test(b2.ice_id() == "::Test::B"); // Sliced by server
                test(b2.pb == b1);

                test(!(b2 instanceof Test.D3));
                test(b1 !== d1);
                test(b1 !== d3);
                test(b2 !== d1);
                test(b2 !== d3);
            }
            out.writeLine("ok");

            out.write("return value identity for input params unknown first... ");
            {
                const d1 = new Test.D1();
                d1.sb = "D1.sb";
                d1.sd1 = "D1.sd1";
                const d3 = new Test.D3();
                d3.pb = d1;
                d3.sb = "D3.sb";
                d3.sd3 = "D3.sd3";
                d3.pd3 = d1;
                d1.pb = d3;
                d1.pd1 = d3;

                const b1 = await prx.returnTest3(d3, d1);

                test(b1 !== null);
                test(b1.sb == "D3.sb");
                test(b1.ice_id() == "::Test::B"); // Sliced by server
                test(!(b1 instanceof Test.D3));

                const b2 = b1.pb;
                test(b2 !== null);
                test(b2.sb == "D1.sb");
                test(b2.ice_id() == "::Test::D1");
                test(b2.pb == b1);

                const p3 = b2;
                test(p3 !== null);
                test(p3.sd1 == "D1.sd1");
                test(p3.pd1 === b1);

                test(b1 !== d1);
                test(b1 !== d3);
                test(b2 !== d1);
                test(b2 !== d3);
            }
            out.writeLine("ok");

            out.write("remainder unmarshaling (3 instances)... ");
            {
                const [ret, p1, p2] = await prx.paramTest3();
                test(p1 !== null);
                test(p1.sb == "D2.sb (p1 1)");
                test(p1.pb === null);
                test(p1.ice_id() == "::Test::B");

                test(p2 !== null);
                test(p2.sb == "D2.sb (p2 1)");
                test(p2.pb === null);
                test(p2.ice_id() == "::Test::B");

                test(ret !== null);
                test(ret.sb == "D1.sb (p2 2)");
                test(ret.pb === null);
                test(ret.ice_id() == "::Test::D1");
            }
            out.writeLine("ok");

            out.write("remainder unmarshaling (4 instances)... ");
            {
                const [ret, b] = await prx.paramTest4();
                test(b !== null);
                test(b.sb == "D4.sb (1)");
                test(b.pb === null);
                test(b.ice_id() == "::Test::B");

                test(ret !== null);
                test(ret.sb == "B.sb (2)");
                test(ret.pb === null);
                test(ret.ice_id() == "::Test::B");
            }
            out.writeLine("ok");

            out.write("param ptr slicing, instance marshaled in unknown derived as base... ");
            {
                const b1 = new Test.B();
                b1.sb = "B.sb(1)";
                b1.pb = b1;

                const d3 = new Test.D3();
                d3.sb = "D3.sb";
                d3.pb = d3;
                d3.sd3 = "D3.sd3";
                d3.pd3 = b1;

                const b2 = new Test.B();
                b2.sb = "B.sb(2)";
                b2.pb = b1;

                const ret = await prx.returnTest3(d3, b2);
                test(ret !== null);
                test(ret.ice_id() == "::Test::B");
                test(ret.sb == "D3.sb");
                test(ret.pb === ret);
            }
            out.writeLine("ok");

            out.write("param ptr slicing, instance marshaled in unknown derived as derived... ");
            {
                const d11 = new Test.D1();
                d11.sb = "D1.sb(1)";
                d11.pb = d11;
                d11.sd1 = "D1.sd1(1)";

                const d3 = new Test.D3();
                d3.sb = "D3.sb";
                d3.pb = d3;
                d3.sd3 = "D3.sd3";
                d3.pd3 = d11;

                const d12 = new Test.D1();
                d12.sb = "D1.sb(2)";
                d12.pb = d12;
                d12.sd1 = "D1.sd1(2)";
                d12.pd1 = d11;

                const ret = await prx.returnTest3(d3, d12);
                test(ret !== null);
                test(ret.ice_id() == "::Test::B");
                test(ret.sb == "D3.sb");
                test(ret.pb === ret);
            }
            out.writeLine("ok");

            out.write("sequence slicing... ");
            {
                let ss1b = new Test.B();
                ss1b.sb = "B.sb";
                ss1b.pb = ss1b;

                let ss1d1 = new Test.D1();
                ss1d1.sb = "D1.sb";
                ss1d1.sd1 = "D1.sd1";
                ss1d1.pb = ss1b;

                let ss1d3 = new Test.D3();
                ss1d3.sb = "D3.sb";
                ss1d3.sd3 = "D3.sd3";
                ss1d3.pb = ss1b;

                let ss2b = new Test.B();
                ss2b.sb = "B.sb";
                ss2b.pb = ss1b;

                let ss2d1 = new Test.D1();
                ss2d1.sb = "D1.sb";
                ss2d1.sd1 = "D1.sd1";
                ss2d1.pb = ss2b;

                let ss2d3 = new Test.D3();
                ss2d3.sb = "D3.sb";
                ss2d3.sd3 = "D3.sd3";
                ss2d3.pb = ss2b;

                ss1d1.pd1 = ss2b;
                ss1d3.pd3 = ss2d1;

                ss2d1.pd1 = ss1d3;
                ss2d3.pd3 = ss1d1;

                const ss1 = new Test.SS1();
                ss1.s = [];
                ss1.s[0] = ss1b;
                ss1.s[1] = ss1d1;
                ss1.s[2] = ss1d3;

                const ss2 = new Test.SS2();
                ss2.s = [];
                ss2.s[0] = ss2b;
                ss2.s[1] = ss2d1;
                ss2.s[2] = ss2d3;

                const ss = await prx.sequenceTest(ss1, ss2);

                test(ss.c1 !== null);
                ss1b = ss.c1.s[0];
                ss1d1 = ss.c1.s[1];
                test(ss.c2 !== null);
                ss1d3 = ss.c1.s[2];

                test(ss.c2 !== null);
                ss2b = ss.c2.s[0];
                ss2d1 = ss.c2.s[1];
                ss2d3 = ss.c2.s[2];

                test(ss1b.pb === ss1b);
                test(ss1d1.pb === ss1b);
                test(ss1d3.pb === ss1b);

                test(ss2b.pb === ss1b);
                test(ss2d1.pb === ss2b);
                test(ss2d3.pb === ss2b);

                test(ss1b.ice_id() == "::Test::B");
                test(ss1d1.ice_id() == "::Test::D1");
                test(ss1d3.ice_id() == "::Test::B");

                test(ss2b.ice_id() == "::Test::B");
                test(ss2d1.ice_id() == "::Test::D1");
                test(ss2d3.ice_id() == "::Test::B");
            }
            out.writeLine("ok");

            out.write("dictionary slicing... ");
            {
                const bin = new Map();
                for(let i = 0; i < 10; ++i)
                {
                    const s = `D1.${i}`;
                    const d1 = new Test.D1();
                    d1.sb = s;
                    d1.pb = d1;
                    d1.sd1 = s;
                    bin.set(i, d1);
                }

                const [ret, boutH] = await prx.dictionaryTest(bin);
                test(boutH.size === 10);
                for(let i = 0; i < 10; ++i)
                {
                    const b = boutH.get(i * 10);
                    test(b !== null);
                    const s = `D1.${i}`;
                    test(b.sb == s);
                    test(b.pb !== null);
                    test(b.pb !== b);
                    test(b.pb.sb == s);
                    test(b.pb.pb == b.pb);
                }

                test(ret.size === 10);
                for(let i = 0; i < 10; ++i)
                {
                    const b = ret.get(i * 20);
                    test(b !== null);
                    const s = "D1." + (i * 20);
                    test(b.sb == s);
                    test(b.pb === (i === 0 ? null : ret.get((i - 1) * 20)));
                    test(b.sd1 == s);
                    test(b.pd1 === b);
                }
            }
            out.writeLine("ok");

            out.write("base exception thrown as base exception... ");
            try
            {
                await prx.throwBaseAsBase();
                test(false);
            }
            catch(ex)
            {
                test(ex instanceof Test.BaseException, ex);
                test(ex.ice_id() == "::Test::BaseException");
                test(ex.sbe == "sbe");
                test(ex.pb !== null);
                test(ex.pb.sb == "sb");
                test(ex.pb.pb == ex.pb);
            }
            out.writeLine("ok");

            out.write("derived exception thrown as base exception... ");
            try
            {
                await prx.throwDerivedAsBase();
                test(false);
            }
            catch(ex)
            {
                test(ex instanceof Test.DerivedException, ex);
                test(ex.ice_id() == "::Test::DerivedException");
                test(ex.sbe == "sbe");
                test(ex.pb !== null);
                test(ex.pb.sb == "sb1");
                test(ex.pb.pb === ex.pb);
                test(ex.sde == "sde1");
                test(ex.pd1 !== null);
                test(ex.pd1.sb == "sb2");
                test(ex.pd1.pb === ex.pd1);
                test(ex.pd1.sd1 == "sd2");
                test(ex.pd1.pd1 === ex.pd1);
            }
            out.writeLine("ok");

            out.write("derived exception thrown as derived exception... ");
            try
            {
                await prx.throwDerivedAsDerived();
                test(false);
            }
            catch(ex)
            {
                test(ex instanceof Test.DerivedException, ex);
                test(ex.ice_id() == "::Test::DerivedException");
                test(ex.sbe == "sbe");
                test(ex.pb !== null);
                test(ex.pb.sb == "sb1");
                test(ex.pb.pb == ex.pb);
                test(ex.sde == "sde1");
                test(ex.pd1 !== null);
                test(ex.pd1.sb == "sb2");
                test(ex.pd1.pb === ex.pd1);
                test(ex.pd1.sd1 == "sd2");
                test(ex.pd1.pd1 === ex.pd1);
            }
            out.writeLine("ok");

            out.write("unknown derived exception thrown as base exception... ");
            try
            {
                await prx.throwUnknownDerivedAsBase();
                test(false);
            }
            catch(ex)
            {
                test(ex instanceof Test.BaseException, ex);
                test(ex.ice_id() == "::Test::BaseException");
                test(ex.sbe == "sbe");
                test(ex.pb !== null);
                test(ex.pb.sb == "sb d2");
                test(ex.pb.pb == ex.pb);
            }
            out.writeLine("ok");

            out.write("forward-declared class... ");
            {
                const f = await prx.useForward();
                test(f !== null);
            }
            out.writeLine("ok");

            out.write("preserved classes... ");
            //
            // Register a factory in order to substitute our own subclass of Preserved. This provides
            // an easy way to determine how many unmarshaled instances currently exist.
            //
            // TODO: We have to install this now (even though it's not necessary yet), because otherwise
            // the Ice run time will install its own internal factory for Preserved upon receiving the
            // first instance.
            //
            communicator.getValueFactoryManager().add(PreservedFactoryI, Test.Preserved.ice_staticId());
            {
                //
                // Server knows the most-derived class PDerived.
                //
                const pd = new Test.PDerived();
                pd.pi = 3;
                pd.ps = "preserved";
                pd.pb = pd;

                const p2 = await prx.exchangePBase(pd);

                test(p2.pi === 3);
                test(p2.ps == "preserved");
                test(p2.pb === p2);
            }

            {
                //
                // Server only knows the base (non-preserved) type, so the object is sliced.
                //
                const pu = new Test.PCUnknown();
                pu.pi = 3;
                pu.pu = "preserved";

                const r = await prx.exchangePBase(pu);

                test(!(r instanceof Test.PCUnknown));
                test(r.pi == 3);
            }

            {
                //
                // Server only knows the intermediate type Preserved. The object will be sliced to
                // Preserved for the 1.0 encoding; otherwise it should be returned intact.
                //
                const pcd = new Test.PCDerived();
                pcd.pi = 3;
                pcd.pbs = [pcd];

                const r = await prx.exchangePBase(pcd);
                if(prx.ice_getEncodingVersion().equals(Ice.Encoding_1_0))
                {
                    test(!(r instanceof Test.PCDerived));
                    test(r.pi === 3);
                }
                else
                {
                    test(r.pi === 3);
                    test(r.pbs[0] === r);
                }
            }

            {
                //
                // Server only knows the intermediate type Preserved. The object will be sliced to
                // Preserved for the 1.0 encoding; otherwise it should be returned intact.
                //
                const pcd = new Test.CompactPCDerived();
                pcd.pi = 3;
                pcd.pbs = [pcd];

                const r = await prx.exchangePBase(pcd);
                if(prx.ice_getEncodingVersion().equals(Ice.Encoding_1_0))
                {
                    test(!(r instanceof Test.CompactPCDerived));
                    test(r.pi === 3);
                }
                else
                {
                    const p2 = r;
                    test(p2.pi === 3);
                    test(p2.pbs[0] === p2);
                }
            }

            {
                //
                // Send an object that will have multiple preserved slices in the server.
                // The object will be sliced to Preserved for the 1.0 encoding.
                //
                const pcd = new Test.PCDerived3();
                pcd.pi = 3;

                //
                // Sending more than 254 objects exercises the encoding for object ids.
                //
                pcd.pbs = new Array(300);
                for(let i = 0; i < 300; ++i)
                {
                    const p2 = new Test.PCDerived2();
                    p2.pi = i;
                    p2.pbs = [null]; // Nil reference. This slice should not have an indirection table.
                    p2.pcd2 = i;
                    pcd.pbs[i] = p2;
                }
                pcd.pcd2 = pcd.pi;
                pcd.pcd3 = pcd.pbs[10];

                const r = await prx.exchangePBase(pcd);
                if(prx.ice_getEncodingVersion().equals(Ice.Encoding_1_0))
                {
                    test(!(r instanceof Test.PCDerived3));
                    test(r instanceof Test.Preserved);
                    test(r.pi === 3);
                }
                else
                {
                    const p3 = r;
                    test(p3.pi === 3);
                    for(let i = 0; i < 300; ++i)
                    {
                        const p2 = p3.pbs[i];
                        test(p2.pi === i);
                        test(p2.pbs.length === 1);
                        test(p2.pbs[0] === null);
                        test(p2.pcd2 === i);
                    }
                    test(p3.pcd2 === p3.pi);
                    test(p3.pcd3 === p3.pbs[10]);
                }
            }

            {
                //
                // Obtain an object with preserved slices and send it back to the server.
                // The preserved slices should be excluded for the 1.0 encoding, otherwise
                // they should be included.
                //
                const p = await prx.PBSUnknownAsPreserved();
                await prx.checkPBSUnknown(p);

                if(!prx.ice_getEncodingVersion().equals(Ice.Encoding_1_0))
                {
                    const slicedData = p.ice_getSlicedData();
                    test(slicedData !== null);
                    test(slicedData.slices.length === 1);
                    test(slicedData.slices[0].typeId == "::Test::PSUnknown");
                    await prx.ice_encodingVersion(Ice.Encoding_1_0).checkPBSUnknown(p);
                }
                else
                {
                    test(p.ice_getSlicedData() === null);
                }
            }
            out.writeLine("ok");
            await prx.shutdown();
        }

        async run(args)
        {
            let communicator;
            try
            {
                [communicator] = this.initialize(args);
                await this.allTests();
            }
            finally
            {
                if(communicator)
                {
                    await communicator.destroy();
                }
            }
        }
    }
    exports.Client = Client;

}(typeof global !== "undefined" && typeof global.process !== "undefined" ? module : undefined,
  typeof global !== "undefined" && typeof global.process !== "undefined" ? require :
  (typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope) ? self.Ice._require : window.Ice._require,
  typeof global !== "undefined" && typeof global.process !== "undefined" ? exports :
  (typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope) ? self : window));
