package com.atlassian.stash.internal.backup.liquibase;

import com.atlassian.fugue.Effect;
import com.atlassian.bitbucket.util.CancelState;
import com.google.common.collect.ImmutableMap;
import liquibase.change.ColumnConfig;
import liquibase.change.core.InsertDataChange;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import javax.xml.stream.XMLStreamException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import static com.atlassian.stash.internal.backup.liquibase.LiquibaseConstants.ENCODING;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class XmlRoundTripTest {
    private static final String NASTY_STRING = "      <?xml version=\"1.0\"?><note><![CDATA[\n" +
            "function matchwo(a,b)\n{     \nif (a < b && a < 0) then\n  {\n" +
            "  return 1; \\    \\u0043  \\\\ \\\\usdfgsfg  \n  }\r\n\telse\n" +
            "  {\n  return 0;\\n  }\n}&#40;\n]]></note>    ";

    @Mock
    private CancelState cancelState;
    @Mock
    private LiquibaseBackupMonitor backupMonitor;
    @Mock
    private LiquibaseDao dao;
    @Captor
    private ArgumentCaptor<InsertDataChange> insertCaptor;
    private Map<String, Object> originalRow;
    @Mock
    private LiquibaseRestoreMonitor restoreMonitor;

    @Before
    @SuppressWarnings("unchecked")
    public void setUp() {
        Answer<Void> applyEffect = new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                Effect<LiquibaseDao> effect = (Effect<LiquibaseDao>) invocation.getArguments()[0];
                effect.apply(dao);
                return null;
            }
        };
        doAnswer(applyEffect).when(dao).withLock(any(Effect.class));
        doReturn("unknown").when(dao).getDatabaseType();
    }

    @Test
    public void testRoundTrip() throws XMLStreamException, UnsupportedEncodingException {
        // Insert some values
        ImmutableMap.Builder<String, Object> rowBuilder = ImmutableMap.builder();
        rowBuilder.put("empty", "");
        rowBuilder.put("integer", "1234567890");
        rowBuilder.put("text", "Just some normal text");
        rowBuilder.put("complex", NASTY_STRING);
        rowBuilder.put("cdata", "]]><node>text</node>");
        rowBuilder.put("cdata2", "<![CDATA[qwe]]>");
        rowBuilder.put("cdata3", "]]]]>> qwe");
        rowBuilder.put("cdata4", "\\]\\]\\> qwe");
        rowBuilder.put("cdata5", "[[[]]] qwe");
        rowBuilder.put("html-encoding", "%5D%5D%3E qwe");          // ]]> but in HTML encoding
        rowBuilder.put("html-encoding2", "%5D%5D&gt; qwe");          // ]]> but in HTML encoding
        rowBuilder.put("international", "hellø костя");
        rowBuilder.put("special-characters", "~`!@#$%^&*()_+-={}|[]\\:\";'<>?,./");
        rowBuilder.put("large-string", StringUtils.repeat("text", 4000));
        rowBuilder.put("utf-encoded", "\\u005d\\u005d\\u003e qwe");
        rowBuilder.put("ascii-encoded", "&#093;&#093;&#062; qwe");          // ]]> in ASCII

        originalRow = rowBuilder.build();

        // Do the round trip
        deserialize(serializeRow(), dao);

        // Get row after round trip
        verify(dao).insert(insertCaptor.capture());
        InsertDataChange insertion = insertCaptor.getValue();
        List<ColumnConfig> actualRow = insertion.getColumns();

        // Assert that whatever we inserted remain intact after round trip
        Iterator<Map.Entry<String, Object>> originalIterator = originalRow.entrySet().iterator();
        Iterator<ColumnConfig> transformedIterator = actualRow.iterator();
        while (originalIterator.hasNext() && transformedIterator.hasNext()) {
            Map.Entry<String, Object> original = originalIterator.next();
            assertEquals("Round trip failed for column:" + original.getKey(), original.getValue(), transformedIterator.next().getValue());
        }
    }

    //------------------------------------------------------------------------------------------------------------------
    // Helper methods used by the test
    //------------------------------------------------------------------------------------------------------------------

    private void deserialize(String changeLog, LiquibaseDao dao) throws UnsupportedEncodingException {
        InputStream in = new ByteArrayInputStream(changeLog.getBytes(ENCODING));
        new DefaultLiquibaseMigrationDao(dao, mock(LiquibaseXmlWriterFactory.class))
                .restore(in, null, restoreMonitor, cancelState);
    }

    private String serializeRow() throws XMLStreamException, UnsupportedEncodingException {
        DefaultLiquibaseXmlWriterFactory writerFactory = new DefaultLiquibaseXmlWriterFactory();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        LiquibaseXmlWriter writer = writerFactory.create(buffer, "test.user");

        writer.writeStartDocument(ENCODING, "1.0");
        writer.writeDatabaseChangeLogStartElement();
        writer.writeChangeSetStartElement("test");
        new WriteXmlForRowEffect(writer, "test", originalRow.keySet(), backupMonitor).apply(originalRow);
        writer.writeEndDocument(); // Closes any start tags and writes corresponding end tags

        writer.flush();
        writer.close();

        return buffer.toString(ENCODING);
    }
}
