/*******************************************************************************
 * Copyright 2017 xlate.io LLC, http://www.xlate.io
 *
 * Licensed 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 io.xlate.edi.internal.stream;

import static io.xlate.edi.test.StaEDITestUtil.normalizeLines;
import static io.xlate.edi.test.StaEDITestUtil.write;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;

import io.xlate.edi.internal.schema.SchemaUtils;
import io.xlate.edi.schema.EDIReference;
import io.xlate.edi.schema.EDISchemaException;
import io.xlate.edi.schema.Schema;
import io.xlate.edi.schema.SchemaFactory;
import io.xlate.edi.stream.EDIInputFactory;
import io.xlate.edi.stream.EDIOutputErrorReporter;
import io.xlate.edi.stream.EDIOutputFactory;
import io.xlate.edi.stream.EDIStreamConstants;
import io.xlate.edi.stream.EDIStreamConstants.Delimiters;
import io.xlate.edi.stream.EDIStreamConstants.Standards;
import io.xlate.edi.stream.EDIStreamEvent;
import io.xlate.edi.stream.EDIStreamException;
import io.xlate.edi.stream.EDIStreamReader;
import io.xlate.edi.stream.EDIStreamValidationError;
import io.xlate.edi.stream.EDIStreamWriter;
import io.xlate.edi.stream.EDIValidationException;
import io.xlate.edi.stream.Location;
import io.xlate.edi.test.StaEDITestUtil;

@SuppressWarnings("resource")
class StaEDIStreamWriterTest {

    private final String TEST_HEADER_X12 = "ISA*00*          *00*          *ZZ*ReceiverID     *ZZ*Sender         *050812*1953*^*00501*508121953*0*P*:~";

    private void writeHeader(EDIStreamWriter writer) throws EDIStreamException {
        // TEST_HEADER_X12
        writer.writeStartSegment("ISA");
        writer.writeElement("00").writeElement("          ");
        writer.writeElement("00").writeElement("          ");
        writer.writeElement("ZZ").writeElement("ReceiverID     ");
        writer.writeElement("ZZ").writeElement("Sender         ");
        writer.writeElement("050812");
        writer.writeElement("1953");
        writer.writeElement("^");
        writer.writeElement("00501");
        writer.writeElement("508121953");
        writer.writeElement("0");
        writer.writeElement("P");
        writer.writeElement(":");
        writer.writeEndSegment();
    }

    static void unconfirmedBufferEquals(String expected, EDIStreamWriter writer) {
        StaEDIStreamWriter writerImpl = (StaEDIStreamWriter) writer;
        writerImpl.unconfirmedBuffer.mark();
        writerImpl.unconfirmedBuffer.flip();
        assertEquals(expected, writerImpl.unconfirmedBuffer.toString());
    }

    @Test
    void testGetProperty() {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        factory.setProperty(EDIStreamConstants.Delimiters.SEGMENT, '~');
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        Object segmentTerminator = writer.getProperty(EDIStreamConstants.Delimiters.SEGMENT);
        assertEquals(Character.valueOf('~'), segmentTerminator);
    }

    @Test
    void testGetNullProperty() {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(1);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        assertThrows(IllegalArgumentException.class, () -> writer.getProperty(null));
    }

    @Test
    void testInvalidBooleanPropertyIsFalse() {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        factory.setProperty(EDIOutputFactory.TRUNCATE_EMPTY_ELEMENTS, new Object());
        OutputStream stream = new ByteArrayOutputStream(1);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);

        assertTrue(writer instanceof StaEDIStreamWriter);
        assertEquals(false, ((StaEDIStreamWriter) writer).emptyElementTruncation);
    }

    @Test
    void testGetDelimitersIllegal() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        assertThrows(IllegalStateException.class, () -> writer.getDelimiters());
    }

    @Test
    void testStartInterchange() {
        try {
            EDIOutputFactory factory = EDIOutputFactory.newFactory();
            OutputStream stream = new ByteArrayOutputStream(4096);
            EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
            writer.startInterchange();
        } catch (Exception e) {
            fail("Unexpected exception: " + e.getMessage());
        }
    }

    @ParameterizedTest
    @ValueSource(strings = { "ISA", "UNA", "UNB", "STX" })
    void testStartInterchangeIllegal(String interchangeStartSegmentTag) throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment(interchangeStartSegmentTag);
        assertThrows(IllegalStateException.class, () -> writer.startInterchange());
    }

    @Test
    void testEndInterchange() {
        try {
            EDIOutputFactory factory = EDIOutputFactory.newFactory();
            OutputStream stream = new ByteArrayOutputStream(4096);
            EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
            writer.startInterchange();
            writer.endInterchange();
        } catch (Exception e) {
            fail("Unexpected exception: " + e.getMessage());
        }
    }

    @Test
    void testEndInterchangeIllegal() {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        assertThrows(IllegalStateException.class, () -> writer.endInterchange());
    }

    @Test
    void testWriteStartSegment() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.flush();
        unconfirmedBufferEquals("ISA", writer);
    }

    @Test
    void testWriteInvalidHeaderElement() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeElement("00").writeElement("           "); // Too long
        writer.writeElement("00").writeElement("           "); // Too long
        writer.writeElement("ZZ").writeElement("ReceiverID     ");
        writer.writeElement("ZZ").writeElement("Sender         ");
        writer.writeElement("050812");
        writer.writeElement("1953");
        writer.writeElement("^");
        writer.writeElement("00501");
        writer.writeElement("508121953");
        writer.writeElement("0");
        writer.writeElement("P");
        EDIStreamException thrown = assertThrows(EDIStreamException.class, () -> writer.writeElement(":"));
        assertEquals("Failed writing X12 header: Element delimiter '*' required in position 18 of X12 header but not found", thrown.getMessage());
    }

    @Test
    void testWriteStartSegmentIllegal() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement();
        assertThrows(IllegalStateException.class, () -> writer.writeStartSegment("GS"));
    }

    @Test
    void testWriteEndSegment() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement().writeElementData("E1").endElement();
        assertThrows(EDIStreamException.class, () -> writer.writeEndSegment());
        writer.flush();
        unconfirmedBufferEquals("ISA*E1~", writer);
    }

    @Test
    void testWriteEndSegmentIllegal() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        assertThrows(IllegalStateException.class, () -> writer.writeEndSegment());
    }

    @Test
    void testStartComponentIllegal() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        assertThrows(IllegalStateException.class, () -> writer.startComponent());
    }

    @Test
    void testWriteStartElement() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement()
              .endElement()
              .writeStartElement()
              .endElement()
              .writeStartElement()
              .endElement()
              .writeStartElement()
              .endElement();
        writer.flush();
        unconfirmedBufferEquals("ISA****", writer);
    }

    @Test
    void testWriteStartElementIllegal() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement();
        writer.startComponent();
        assertThrows(IllegalStateException.class, () -> writer.writeStartElement());
    }

    @Test
    void testWriteInvalidCharacter() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement();
        EDIStreamException thrown = assertThrows(EDIStreamException.class,
                                                 () -> writer.writeElementData("\u0008\u0010"));
        assertEquals("Invalid character: 0x0008 in segment ISA at position 1, element 1", thrown.getMessage());
    }

    @Test
    void testWriteInvalidCharacterRepeatedComposite() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writeHeader(writer);
        writer.writeStartSegment("FOO");
        writer.writeElement("BAR1");
        writer.writeRepeatElement(); // starts new element
        writer.writeComponent("BAR2");
        writer.writeComponent("BAR3");
        EDIStreamException thrown = assertThrows(EDIStreamException.class,
                                                 () -> writer.writeComponent("\u0008\u0010"));
        assertEquals("Invalid character: 0x0008 in segment FOO at position 2, element 1 (occurrence 2), component 3", thrown.getMessage());
        Location l = thrown.getLocation();
        assertEquals("FOO", l.getSegmentTag());
        assertEquals(2, l.getSegmentPosition());
        assertEquals(1, l.getElementPosition());
        assertEquals(2, l.getElementOccurrence());
        assertEquals(3, l.getComponentPosition());
    }

    @Test
    void testWriteInvalidSegmentTag() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writeHeader(writer);
        writer.writeStartSegment("G");
        EDIStreamException thrown = assertThrows(EDIStreamException.class,
                                                 () -> writer.writeElement("FOO"));
        assertEquals("Invalid state: INVALID; output 0x002A", thrown.getMessage());
    }

    @Test
    void testWriteStartElementBinary() throws IllegalStateException, EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        ByteArrayOutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writeHeader(writer);
        writer.flush();
        stream.reset();
        writer.writeStartSegment("BIN");
        writer.writeStartElementBinary().writeEndSegment();
        writer.flush();
        assertEquals("BIN*~", stream.toString());
    }

    @Test
    void testWriteStartElementBinaryIllegal() throws IllegalStateException, EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement();
        assertThrows(IllegalStateException.class, () -> writer.writeStartElementBinary());
    }

    @Test
    void testWriteBinaryDataIllegal() throws IllegalStateException, EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement();
        ByteBuffer binaryData = ByteBuffer.allocate(1);
        assertThrows(IllegalStateException.class, () -> writer.writeBinaryData(binaryData));
    }

    @Test
    void testStartComponentIllegalInElementBinary() throws IllegalStateException, EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        ByteArrayOutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writeHeader(writer);
        writer.flush();
        stream.reset();
        writer.writeStartSegment("BIN");
        writer.writeStartElementBinary();
        assertThrows(IllegalStateException.class, () -> writer.startComponent());
    }

    @Test
    void testComponent() throws IllegalStateException, EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement()
              .startComponent()
              .endComponent()
              .startComponent();
        assertThrows(EDIStreamException.class, () -> writer.writeEndSegment());
        writer.flush();
        unconfirmedBufferEquals("ISA*:~", writer);
    }

    @Test
    void testComponent_EmptyTruncated() throws IllegalStateException, EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        factory.setProperty(EDIOutputFactory.TRUNCATE_EMPTY_ELEMENTS, true);
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement()
              .startComponent()
              .endComponent()
              .startComponent()
              .endComponent()
              .endElement()
              .writeEmptyElement();
        assertThrows(EDIStreamException.class, () -> writer.writeEndSegment());
        writer.flush();

        unconfirmedBufferEquals("ISA~", writer);
    }

    @Test
    void testComponentIllegal() throws IllegalStateException, EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement();
        writer.startComponent();
        assertThrows(IllegalStateException.class, () -> writer.startComponent()); // Double
    }

    @Test
    void testWriteRepeatElement() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        ByteArrayOutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writeHeader(writer);
        writer.flush();
        stream.reset();
        writer.writeStartSegment("SEG");
        writer.writeStartElement()
              .writeElementData("R1")
              .writeRepeatElement()
              .writeElementData("R2")
              .writeEndSegment();
        writer.flush();
        assertEquals("SEG*R1^R2~", stream.toString());
    }

    @Test
    void testWriteEmptyElement() throws IllegalStateException, EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeEmptyElement();
        writer.writeEmptyElement();
        writer.writeEmptyElement();
        writer.writeEmptyElement();
        assertThrows(EDIStreamException.class, () -> writer.writeEndSegment());
        writer.flush();
        unconfirmedBufferEquals("ISA****~", writer);
    }

    @Test
    void testWriteEmptyComponent() throws IllegalStateException, EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        assertThrows(EDIStreamException.class, () -> writer.writeEndSegment());
        writer.flush();
        unconfirmedBufferEquals("ISA*:::~", writer);
    }

    @Test
    void testWriteEmptyElements_EmptyTruncated() throws IllegalStateException, EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        factory.setProperty(EDIOutputFactory.TRUNCATE_EMPTY_ELEMENTS, true);
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.endElement();
        assertThrows(EDIStreamException.class, () -> writer.writeEndSegment());
        writer.flush();
        unconfirmedBufferEquals("ISA~", writer);
    }

    @Test
    void testWriteTruncatedElements() throws IllegalStateException, EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        factory.setProperty(EDIOutputFactory.TRUNCATE_EMPTY_ELEMENTS, true);
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");

        // All components truncated
        writer.writeStartElement();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.endElement();

        // Components after "LAST" truncated
        writer.writeStartElement();
        writer.writeEmptyComponent();
        writer.writeComponent("LAST");
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.endElement();

        // All present
        writer.writeStartElement();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.writeComponent("LAST");
        writer.endElement();

        // All present
        writer.writeStartElement();
        writer.writeEmptyComponent();
        writer.writeComponent("SECOND");
        writer.writeEmptyComponent();
        writer.writeComponent("LAST");
        writer.endElement();

        writer.writeEmptyElement();
        writer.writeEmptyElement();

        writer.writeElement("LAST");

        // All three elements truncated
        writer.writeEmptyElement();
        writer.writeStartElement();
        writer.writeEmptyComponent();
        writer.writeEmptyComponent();
        writer.endElement();
        writer.writeEmptyElement();

        assertThrows(EDIStreamException.class, () -> writer.writeEndSegment());
        writer.flush();

        unconfirmedBufferEquals("ISA**:LAST*:::LAST*:SECOND::LAST***LAST~", writer);
    }

    @Test
    void testWriteElementDataCharSequence() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement();
        writer.writeElementData("TEST-ELEMENT");
        assertThrows(EDIStreamException.class, () -> writer.writeEndSegment());
        writer.flush();
        unconfirmedBufferEquals("ISA*TEST-ELEMENT~", writer);
    }

    @Test
    void testWriteElementDataCharSequenceIllegal() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writeHeader(writer);
        writer.writeStartSegment("GS");
        writer.writeStartElement();
        assertThrows(IllegalArgumentException.class, () -> writer.writeElementData("** BAD^ELEMENT **"));
    }

    @Test
    void testWriteElementDataCharArray() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writer.writeStartSegment("ISA");
        writer.writeStartElement();
        writer.writeElementData(new char[] { 'C', 'H', 'A', 'R', 'S' }, 0, 5);
        assertThrows(EDIStreamException.class, () -> writer.writeEndSegment());
        writer.flush();
        unconfirmedBufferEquals("ISA*CHARS~", writer);
    }

    @Test
    void testWriteElementDataCharInvalidBoundaries() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writeHeader(writer);
        writer.writeStartSegment("GS");
        writer.writeStartElement();
        assertThrows(IndexOutOfBoundsException.class, () -> writer.writeElementData(new char[] { 'F', 'A' }, -1, 2));
        assertThrows(IndexOutOfBoundsException.class, () -> writer.writeElementData(new char[] { 'F', 'A' }, 2, 1));
        assertThrows(IndexOutOfBoundsException.class, () -> writer.writeElementData(new char[] { 'F', 'A' }, 0, 3));
        assertThrows(IllegalArgumentException.class, () -> writer.writeElementData(new char[] { 'F', 'A' }, 0, -1));
    }

    @Test
    void testWriteElementDataCharArrayIllegal() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        OutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        writer.startInterchange();
        writeHeader(writer);
        writer.writeStartSegment("GS");
        writer.writeStartElement();
        assertThrows(IllegalArgumentException.class, () -> writer.writeElementData(new char[] { 'C', 'H', '~', 'R', 'S' }, 0, 5));
    }

    @Test
    void testWriteBinaryDataInputStream() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        ByteArrayOutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        byte[] binary = { '\n', 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, '\t' };
        InputStream binaryStream = new ByteArrayInputStream(binary);
        writer.startInterchange();
        writeHeader(writer);
        writer.flush();
        stream.reset();
        writer.writeStartSegment("BIN");
        writer.writeStartElement();
        writer.writeElementData("8");
        writer.endElement();
        writer.writeStartElementBinary();
        writer.writeBinaryData(binaryStream);
        writer.endElement();
        writer.writeEndSegment();
        writer.flush();
        assertEquals("BIN*8*\n\u0000\u0001\u0002\u0003\u0004\u0005\t~", stream.toString());
    }

    @Test
    void testWriteBinaryDataInputStreamReadIOException() throws Exception {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        ByteArrayOutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        InputStream binaryStream = Mockito.mock(InputStream.class);
        IOException ioException = new IOException();
        Mockito.when(binaryStream.read()).thenThrow(ioException);
        writer.startInterchange();
        writeHeader(writer);
        stream.reset();
        writer.writeStartSegment("BIN");
        writer.writeStartElement();
        writer.writeElementData("4");
        writer.endElement();
        writer.writeStartElementBinary();
        EDIStreamException thrown = assertThrows(EDIStreamException.class,
                                                 () -> writer.writeBinaryData(binaryStream));
        assertEquals("Exception reading binary data source for output in segment BIN at position 2, element 2",
                     thrown.getMessage());
        assertSame(ioException, thrown.getCause());
    }

    @Test
    void testWriteBinaryDataInputStreamWriteIOException() throws Exception {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        ByteArrayOutputStream stream = Mockito.spy(new ByteArrayOutputStream(4096));
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);

        byte[] binary = { '\n', 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, '\t' };
        InputStream binaryStream = new ByteArrayInputStream(binary);

        AtomicBoolean binaryElementStarted = new AtomicBoolean(false);
        IOException ioException = new IOException();
        Mockito.doAnswer(args -> {
            if (binaryElementStarted.get()) {
                throw ioException;
            }
            args.callRealMethod();
            return null;
        }).when(stream).write(0x00); // fail on second byte of binary stream

        writer.startInterchange();
        writeHeader(writer);
        stream.reset();
        writer.writeStartSegment("BIN");
        writer.writeStartElement();
        writer.writeElementData("4");
        writer.endElement();
        writer.writeStartElementBinary();
        binaryElementStarted.set(true);
        EDIStreamException thrown = assertThrows(EDIStreamException.class,
                                                 () -> writer.writeBinaryData(binaryStream));
        assertEquals("Exception writing binary element data in segment BIN at position 2, element 2",
                     thrown.getMessage());
        assertSame(ioException, thrown.getCause());
    }

    @Test
    void testWriteBinaryDataByteArray() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        ByteArrayOutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        byte[] binary = { '\n', 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, '\t' };
        writer.startInterchange();
        writeHeader(writer);
        writer.flush();
        stream.reset();
        writer.writeStartSegment("BIN");
        writer.writeStartElement();
        writer.writeElementData("8");
        writer.endElement();
        writer.writeStartElementBinary();
        writer.writeBinaryData(binary, 0, binary.length);
        writer.endElement();
        writer.writeEndSegment();
        writer.flush();
        assertEquals("BIN*8*\n\u0000\u0001\u0002\u0003\u0004\u0005\t~", stream.toString());
    }

    @Test
    void testWriteBinaryDataByteBuffer() throws EDIStreamException {
        EDIOutputFactory factory = EDIOutputFactory.newFactory();
        ByteArrayOutputStream stream = new ByteArrayOutputStream(4096);
        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
        byte[] binary = { '\n', 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, '\t' };
        ByteBuffer buffer = ByteBuffer.wrap(binary);
        writer.startInterchange();
        writeHeader(writer);
        writer.flush();
        stream.reset();
        writer.writeStartSegment("BIN");
        writer.writeStartElement();
        writer.writeElementData("8");
        writer.endElement();
        writer.writeStartElementBinary();
        writer.writeBinaryData(buffer);
