/*
 * Copyright (c) 2015, 2023, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, version 2.0, as published by the
 * Free Software Foundation.
 *
 * This program is also distributed with certain software (including but not
 * limited to OpenSSL) that is licensed under separate terms, as designated in a
 * particular file or component or in included license documentation. The
 * authors of MySQL hereby grant you an additional permission to link the
 * program and your derivative works with the separately licensed software that
 * they have included with MySQL.
 *
 * Without limiting anything contained in the foregoing, this file, which is
 * part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
 * version 1.0, a copy of which can be found at
 * http://oss.oracle.com/licenses/universal-foss-exception.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0,
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
 */

package com.mysql.cj.protocol.x;

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 static org.junit.jupiter.api.Assertions.fail;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Map;

import org.junit.jupiter.api.Test;

import com.google.protobuf.GeneratedMessageV3;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Parser;
import com.mysql.cj.exceptions.WrongArgumentException;
import com.mysql.cj.protocol.FullReadInputStream;
import com.mysql.cj.x.protobuf.Mysqlx.Error;
import com.mysql.cj.x.protobuf.Mysqlx.Ok;
import com.mysql.cj.x.protobuf.Mysqlx.ServerMessages;

/**
 * Tests for {@link SyncMessageReader}.
 */
public class SyncMessageReaderTest {

    private SyncMessageReader reader;

    private static final byte[] okMsgPacket = serializeMessage(Ok.newBuilder().build(), ServerMessages.Type.OK_VALUE);
    private static final byte[] errMsgPacket = serializeMessage(
            Error.newBuilder().setMsg("oops").setCode(5432).setSqlState("12S34").setSeverity(Error.Severity.FATAL).build(), ServerMessages.Type.ERROR_VALUE);

    /**
     * Serialize a message for testing.
     *
     * @param msg
     * @param type
     * @return a byte array
     */
    private static byte[] serializeMessage(GeneratedMessageV3 msg, int type) {
        int packetLen = msg.getSerializedSize() + 1;
        byte[] packet = ByteBuffer.allocate(packetLen + 4).order(ByteOrder.LITTLE_ENDIAN).putInt(packetLen).put((byte) type).put(msg.toByteArray()).array();
        return packet;
    }

    @Test
    public void testNextMessageClass() throws IOException {
        this.reader = new SyncMessageReader(new FullReadInputStream(new ByteArrayInputStream(okMsgPacket)), null);
        assertEquals(Ok.class, MessageConstants.getMessageClassForType(this.reader.readHeader().getMessageType()));
    }

    @Test
    public void testReadKnownMessageType() {
        try {
            this.reader = new SyncMessageReader(new FullReadInputStream(new ByteArrayInputStream(okMsgPacket)), null);
            Ok msg = (Ok) this.reader.readMessage(null, ServerMessages.Type.OK_VALUE).getMessage();
            assertTrue(msg.isInitialized());
        } catch (IOException e) {
            throw new XProtocolError(e.getMessage(), e);
        }
    }

    @Test
    public void testReadWrongMessageType() {
        this.reader = new SyncMessageReader(new FullReadInputStream(new ByteArrayInputStream(okMsgPacket)), null);
        // will throw a WrongArgumentException if failed
        try {
            Error msg = (Error) this.reader.readMessage(null, ServerMessages.Type.ERROR_VALUE).getMessage();
            fail("Should not be able to read an error message when one is not present");
            assertTrue(msg.isInitialized()); // to squelch compiler warnings
        } catch (WrongArgumentException ex) {
            assertEquals("Unexpected message class. Expected '" + Error.class.getSimpleName() + "' but actually received '" + Ok.class.getSimpleName() + "'",
                    ex.getMessage());
        } catch (IOException e) {
            throw new XProtocolError(e.getMessage(), e);
        }
    }

    @Test
    public void testUnexpectedError() {
        this.reader = new SyncMessageReader(new FullReadInputStream(new ByteArrayInputStream(errMsgPacket)), null);
        try {
            // attempt to read an Ok packet
            this.reader.readMessage(null, ServerMessages.Type.OK_VALUE);
            fail("Should not be able to read the OK packet");
        } catch (IOException e) {
            throw new XProtocolError(e.getMessage(), e);
        } catch (XProtocolError ex) {
            // check that the exception contains the error info from the server
            assertEquals("ERROR 5432 (12S34) oops", ex.getMessage());
            assertEquals("12S34", ex.getSQLState());
            assertEquals(5432, ex.getErrorCode());
        }
    }

    /**
     * This is a 'mini'-stress test that encompasses the check of <i>clearHeader()</i> being called correctly.
     *
     * @throws IOException
     */
    @Test
    public void testSeveralMessages() throws IOException {
        // construct the test message stream
        // message stream is: Error, Error, Error, Ok, Error, Ok, Error
        // if the header is not cleared properly, the second Error would be read incorrectly
        ByteArrayOutputStream x = new ByteArrayOutputStream();
        x.write(errMsgPacket);
        x.write(errMsgPacket);
        x.write(errMsgPacket);
        x.write(okMsgPacket);
        x.write(errMsgPacket);
        x.write(okMsgPacket);
        x.write(errMsgPacket);

        this.reader = new SyncMessageReader(new FullReadInputStream(new ByteArrayInputStream(x.toByteArray())), null);
        // read first three errors "unexpectedly" in a loop
        for (int i = 0; i < 3; ++i) {
            try {
                this.reader.readMessage(null, ServerMessages.Type.OK_VALUE);
            } catch (XProtocolError err) {
                assertEquals(5432, err.getErrorCode());
            }
        }
        // read remaining messages normally
        this.reader.readMessage(null, ServerMessages.Type.OK_VALUE);
        try {
            this.reader.readMessage(null, ServerMessages.Type.ERROR_VALUE);
        } catch (XProtocolError err) {
            // expected
        }
        this.reader.readMessage(null, ServerMessages.Type.OK_VALUE);
        try {
            this.reader.readMessage(null, ServerMessages.Type.ERROR_VALUE);
        } catch (XProtocolError err) {
            // expected
        }
    }

    /**
     * Verification test to help prevent bugs in the typecode/class/parser mapping tables. We check that all classes that are mapped have a parser.
     *
     * @throws InvalidProtocolBufferException
     *
     * @todo Test in the other direction also
     */
    @Test
    public void testMappingTables() throws InvalidProtocolBufferException {
        for (Map.Entry<Class<? extends GeneratedMessageV3>, Integer> entry : MessageConstants.MESSAGE_CLASS_TO_TYPE.entrySet()) {
            /* int type = */entry.getValue();
            Class<? extends GeneratedMessageV3> messageClass = entry.getKey();
            Parser<? extends GeneratedMessageV3> parser = MessageConstants.MESSAGE_CLASS_TO_PARSER.get(messageClass);
            assertNotNull(parser);
            GeneratedMessageV3 partiallyParsed = parser.parsePartialFrom(new byte[] {});
            assertEquals(messageClass, partiallyParsed.getClass(), "Parsed class should equal the class that mapped to it via type tag");
        }
    }

}
