/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.qpid.protonj2.test.driver.codec;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;

import org.apache.qpid.protonj2.test.driver.codec.messaging.Source;
import org.apache.qpid.protonj2.test.driver.codec.messaging.Target;
import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
import org.apache.qpid.protonj2.test.driver.codec.transport.Begin;
import org.apache.qpid.protonj2.test.driver.codec.transport.Open;
import org.apache.qpid.protonj2.test.driver.codec.transport.ReceiverSettleMode;
import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
import org.apache.qpid.protonj2.test.driver.codec.transport.SenderSettleMode;
import org.junit.jupiter.api.Test;

/**
 * Test some basic operations of the Data type codec
 */
public class DataImplTest {

    private final Codec codec = Codec.Factory.create();

    @Test
    public void testDecodeOpen() {
        Open open = new Open();
        open.setContainerId("test");
        open.setHostname("localhost");

        ByteBuffer encoded = encodeProtonPerformative(open);
        int expectedRead = encoded.remaining();

        Codec codec = Codec.Factory.create();

        assertEquals(expectedRead, codec.decode(encoded));

        Open described = (Open) codec.getDescribedType();
        assertNotNull(described);
        assertEquals(Open.DESCRIPTOR_SYMBOL, described.getDescriptor());

        assertEquals(open.getContainerId(), described.getContainerId());
        assertEquals(open.getHostname(), described.getHostname());
    }

    @Test
    public void testEncodeOpen() throws IOException {
        Open open =new Open();
        open.setContainerId("test");
        open.setHostname("localhost");

        Codec codec = Codec.Factory.create();

        codec.putDescribedType(open);
        final long encodedSizeEstimate = codec.encodedSize();

        final ByteArrayOutputStream baos = new ByteArrayOutputStream(256);
        final DataOutputStream output = new DataOutputStream(baos);

        codec.encode(output);

        final ByteBuffer encoded = ByteBuffer.wrap(baos.toByteArray());

        assertEquals(encodedSizeEstimate, encoded.remaining());

        DescribedType decoded = decodeProtonPerformative(encoded);
        assertNotNull(decoded);
        assertTrue(decoded instanceof Open);

        Open performative = (Open) decoded;
        assertEquals(open.getContainerId(), performative.getContainerId());
        assertEquals(open.getHostname(), performative.getHostname());
    }

    @Test
    public void testDecodeBegin() {
        Begin begin = new Begin();
        begin.setHandleMax(UnsignedInteger.valueOf(512));
        begin.setRemoteChannel(UnsignedShort.valueOf(1));

        ByteBuffer encoded = encodeProtonPerformative(begin);
        int expectedRead = encoded.remaining();

        Codec codec = Codec.Factory.create();

        assertEquals(expectedRead, codec.decode(encoded));

        Begin described = (Begin) codec.getDescribedType();
        assertNotNull(described);
        assertEquals(Begin.DESCRIPTOR_SYMBOL, described.getDescriptor());

        assertEquals(described.getHandleMax(), UnsignedInteger.valueOf(512));
        assertEquals(described.getRemoteChannel(), UnsignedShort.valueOf((short) 1));
    }

    @Test
    public void testEncodeBegin() throws IOException {
        Begin begin = new Begin();
        begin.setHandleMax(UnsignedInteger.valueOf(512));
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 1));
        begin.setIncomingWindow(UnsignedInteger.valueOf(2));
        begin.setNextOutgoingId(UnsignedInteger.valueOf(2));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(3));

        Codec codec = Codec.Factory.create();

        codec.putDescribedType(begin);

        try (ByteArrayOutputStream baos = new ByteArrayOutputStream((int) codec.encodedSize());
             DataOutputStream output = new DataOutputStream(baos)) {

            codec.encode(output);

            final ByteBuffer encoded = ByteBuffer.wrap(baos.toByteArray());

            DescribedType decoded = decodeProtonPerformative(encoded);
            assertNotNull(decoded);
            assertTrue(decoded instanceof Begin);

            Begin performative = (Begin) decoded;
            assertEquals(performative.getHandleMax(), UnsignedInteger.valueOf(512));
            assertEquals(performative.getRemoteChannel(), UnsignedShort.valueOf((short) 1));
        }
    }

    @Test
    public void testDecodeAttach() {
        Attach attach = new Attach();
        attach.setName("test");
        attach.setHandle(UnsignedInteger.valueOf(1));
        attach.setRole(Role.SENDER);
        attach.setSenderSettleMode(SenderSettleMode.MIXED);
        attach.setReceiverSettleMode(ReceiverSettleMode.FIRST);
        attach.setSource(new Source());
        attach.setTarget(new Target());

        final ByteBuffer encoded = encodeProtonPerformative(attach);
        final int expectedRead = encoded.remaining();

        Codec codec = Codec.Factory.create();

        assertEquals(expectedRead, codec.decode(encoded));

        Attach described = (Attach) codec.getDescribedType();
        assertNotNull(described);
        assertEquals(Attach.DESCRIPTOR_SYMBOL, described.getDescriptor());

        assertEquals(described.getHandle(), UnsignedInteger.valueOf(1));
        assertEquals(described.getName(), "test");
    }

    @Test
    public void testEncodeAttach() throws IOException {
        Attach attach = new Attach();
        attach.setName("test");
        attach.setHandle(UnsignedInteger.valueOf(1));
        attach.setRole(Role.SENDER.getValue());
        attach.setSenderSettleMode(SenderSettleMode.MIXED.getValue());
        attach.setReceiverSettleMode(ReceiverSettleMode.FIRST.getValue());
        attach.setSource(new Source());
        attach.setTarget(new Target());

        Codec codec = Codec.Factory.create();

        codec.putDescribedType(attach);

        try (ByteArrayOutputStream baos = new ByteArrayOutputStream((int) codec.encodedSize());
             DataOutputStream output = new DataOutputStream(baos)) {

           codec.encode(output);

           final ByteBuffer encoded = ByteBuffer.wrap(baos.toByteArray());

            DescribedType decoded = decodeProtonPerformative(encoded);
            assertNotNull(decoded);
            assertTrue(decoded instanceof Attach);

            Attach performative = (Attach) decoded;
            assertEquals(performative.getHandle(), UnsignedInteger.valueOf(1));
            assertEquals(performative.getName(), "test");
        }
    }

    @Test
    public void testDecodeOfComplexOpenEncoding() throws IOException {
        // Frame data for: Open
        //   Open{ containerId='container', hostname='localhost', maxFrameSize=16384, channelMax=65535,
        //         idleTimeOut=36000, outgoingLocales=null, incomingLocales=null, offeredCapabilities=[SOMETHING],
        //         desiredCapabilities=[ANONYMOUS-RELAY, DELAYED-DELIVERY], properties={queue-prefix=queue://}}
        final byte[] completeOpen = new byte[] {0, 83, 16, -64, 116, 10, -95, 9, 99, 111, 110, 116, 97, 105, 110,
                                                101, 114, -95, 9, 108, 111, 99, 97, 108, 104, 111, 115, 116, 112,
                                                0, 0, 64, 0, 96, -1, -1, 112, 0, 0, -116, -96, 64, 64, -32, 12, 1,
                                                -93, 9, 83, 79, 77, 69, 84, 72, 73, 78, 71, -32, 35, 2, -93, 15,
                                                65, 78, 79, 78, 89, 77, 79, 85, 83, 45, 82, 69, 76, 65, 89, 16, 68,
                                                69, 76, 65, 89, 69, 68, 45, 68, 69, 76, 73, 86, 69, 82, 89, -63, 25,
                                                2, -95, 12, 113, 117, 101, 117, 101, 45, 112, 114, 101, 102, 105,
                                                120, -95, 8, 113, 117, 101, 117, 101, 58, 47, 47};

        final ByteBuffer encoded = ByteBuffer.wrap(completeOpen);

        DescribedType decoded = decodeProtonPerformative(encoded);
        assertNotNull(decoded);
        assertTrue(decoded instanceof Open);

        final Open performative = (Open) decoded;

        assertEquals("container", performative.getContainerId());
        assertEquals("localhost", performative.getHostname());
        assertEquals(UnsignedInteger.valueOf(16384), performative.getMaxFrameSize());
        assertEquals(UnsignedInteger.valueOf(36000), performative.getIdleTimeOut());
        assertEquals(UnsignedShort.valueOf(65535), performative.getChannelMax());

        Symbol[] offered = performative.getOfferedCapabilities();
        Symbol[] desired = performative.getDesiredCapabilities();

        assertArrayEquals(new Symbol[] { Symbol.valueOf("SOMETHING") }, offered);
        assertArrayEquals(new Symbol[] { Symbol.valueOf("ANONYMOUS-RELAY"), Symbol.valueOf("DELAYED-DELIVERY") }, desired);

        Map<String, Object> expected = new HashMap<String, Object>();
        expected.put("queue-prefix", "queue://");

        assertEquals(expected, performative.getProperties());
    }

    private DescribedType decodeProtonPerformative(ByteBuffer buffer) throws IOException {
        DescribedType performative = null;

        try {
            codec.decode(buffer);
        } catch (Exception e) {
            throw new AssertionError("Decoder failed reading remote input:", e);
        }

        Codec.DataType dataType = codec.type();
        if (dataType != Codec.DataType.DESCRIBED) {
            throw new IllegalArgumentException(
                "Frame body type expected to be " + Codec.DataType.DESCRIBED + " but was: " + dataType);
        }

        try {
            performative = codec.getDescribedType();
        } finally {
            codec.clear();
        }

        return performative;
    }

    private ByteBuffer encodeProtonPerformative(DescribedType performative) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(256);

        if (performative != null) {
            try (DataOutputStream output = new DataOutputStream(baos)) {
                codec.putDescribedType(performative);
                codec.encode(output);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            } finally {
                codec.clear();
            }
        }

        return ByteBuffer.wrap(baos.toByteArray());
    }
}
