/*
 * Copyright 2015-2020 Real Logic Limited., Monotonic Ltd.
 *
 * 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
 *
 * https://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 uk.co.real_logic.artio.engine.framer;

import org.agrona.ErrorHandler;
import org.agrona.IoUtil;
import org.agrona.concurrent.AtomicBuffer;
import org.agrona.concurrent.UnsafeBuffer;
import org.junit.Test;
import uk.co.real_logic.artio.FileSystemCorruptionException;
import uk.co.real_logic.artio.builder.LogonEncoder;
import uk.co.real_logic.artio.dictionary.FixDictionary;
import uk.co.real_logic.artio.engine.EngineConfiguration;
import uk.co.real_logic.artio.engine.MappedFile;
import uk.co.real_logic.artio.fixt.FixDictionaryImpl;
import uk.co.real_logic.artio.session.CompositeKey;
import uk.co.real_logic.artio.session.SessionIdStrategy;
import uk.co.real_logic.artio.util.MutableAsciiBuffer;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.stream.IntStream;

import static java.util.stream.Collectors.toList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.*;
import static uk.co.real_logic.artio.engine.EngineConfiguration.DEFAULT_INITIAL_SEQUENCE_INDEX;
import static uk.co.real_logic.artio.engine.framer.SessionContexts.DUPLICATE_SESSION;
import static uk.co.real_logic.artio.engine.framer.SessionContexts.LOWEST_VALID_SESSION_ID;

public class SessionContextsTest
{
    private static final int BUFFER_SIZE = 8 * 1024;
    private static final int TEST_INITIAL_SEQUENCE = 721;

    private final long time = System.currentTimeMillis();
    private final ErrorHandler errorHandler = mock(ErrorHandler.class);
    private final AtomicBuffer buffer = new UnsafeBuffer(ByteBuffer.allocate(BUFFER_SIZE));
    private final MappedFile mappedFile = mock(MappedFile.class);
    private final SessionIdStrategy idStrategy = SessionIdStrategy.senderAndTarget();
    private SessionContexts sessionContexts = newSessionContexts(buffer);
    private final MutableAsciiBuffer asciiBuffer = new MutableAsciiBuffer(ByteBuffer.allocate(BUFFER_SIZE));
    private final LogonEncoder logonEncoder = new LogonEncoder();
    private final FixDictionary fixDictionary = FixDictionary.of(FixDictionary.findDefault());

    private final CompositeKey aSession = idStrategy.onInitiateLogon("a", null, null, "b", null, null);
    private final CompositeKey bSession = idStrategy.onInitiateLogon("b", null, null, "a", null, null);
    private final CompositeKey cSession = idStrategy.onInitiateLogon("c", null, null, "c", null, null);
    private final CompositeKey otherSession = idStrategy.onInitiateLogon(
        "acceptor", null, null, "initiator", null, null);

    @Test
    public void sessionContextsAreUnique()
    {
        assertNotEquals(sessionContexts.onLogon(aSession, fixDictionary),
            sessionContexts.onLogon(bSession, fixDictionary));
    }

    @Test
    public void findsDuplicateSessions()
    {
        sessionContexts.onLogon(aSession, fixDictionary);

        assertEquals(SessionContexts.DUPLICATE_SESSION, sessionContexts.onLogon(aSession, fixDictionary));
    }

    @Test
    public void handsOutSameSessionContextAfterDisconnect()
    {
        final SessionContext sessionContext = sessionContexts.onLogon(aSession, fixDictionary);
        sessionContexts.onDisconnect(sessionContext.sessionId());

        assertValuesEqual(sessionContext, sessionContexts.onLogon(aSession, fixDictionary));
    }

    @Test
    public void persistsSessionContextsOverARestart()
    {
        final SessionContext bContext = sessionContexts.onLogon(bSession, fixDictionary);
        final SessionContext aContext = sessionContexts.onLogon(aSession, fixDictionary);

        bContext.onSequenceReset(time);
        aContext.onSequenceReset(time);

        final SessionContexts sessionContextsAfterRestart = newSessionContexts(buffer);
        final SessionContext reloadedAContext = sessionContextsAfterRestart.onLogon(aSession, fixDictionary);
        final SessionContext reloadedBContext = sessionContextsAfterRestart.onLogon(bSession, fixDictionary);

        assertValuesEqual(aContext, reloadedAContext);
        assertValuesEqual(bContext, reloadedBContext);

        assertEquals(time, reloadedAContext.lastSequenceResetTime());
        assertEquals(time, reloadedBContext.lastSequenceResetTime());
    }

    @Test
    public void sessionPersistedCorrectlyAfterARestart()
    {
        sessionContexts.onLogon(aSession, fixDictionary);
        final SessionContexts sessionContextsAfterRestart = newSessionContexts(buffer);
        final SessionContext reloadedAContext = sessionContextsAfterRestart.onLogon(aSession, fixDictionary);
        reloadedAContext.onSequenceReset(time + 1);

        final SessionContexts sessionContextsAfterSecondRestart = newSessionContexts(buffer);
        final SessionContext reloadedAgainContext = sessionContextsAfterSecondRestart.onLogon(aSession, fixDictionary);
        assertEquals(time + 1, reloadedAgainContext.lastSequenceResetTime());
    }

    @Test
    public void continuesIncrementingSessionContextsAfterRestart()
    {
        final SessionContext bContext = sessionContexts.onLogon(bSession, fixDictionary);
        final SessionContext aContext = sessionContexts.onLogon(aSession, fixDictionary);

        final SessionContexts sessionContextsAfterRestart = newSessionContexts(buffer);

        final SessionContext cContext = sessionContextsAfterRestart.onLogon(cSession, fixDictionary);
        assertValidSessionId(cContext.sessionId());
        assertNotEquals("C is a duplicate of A", aContext, cContext);
        assertNotEquals("C is a duplicate of B", bContext, cContext);
    }

    @Test
    public void initialSequenceIndex()
    {
        final SessionContexts contexts = newSessionContexts(buffer, TEST_INITIAL_SEQUENCE);
        final SessionContext context = contexts.onLogon(aSession, fixDictionary);
        context.onLogon(false, time, fixDictionary);
        assertEquals(TEST_INITIAL_SEQUENCE, context.sequenceIndex());
        context.onSequenceReset(time);
        assertEquals(TEST_INITIAL_SEQUENCE + 1, context.sequenceIndex());
    }

    @Test
    public void checksFileCorruption()
    {
        sessionContexts.onLogon(bSession, fixDictionary);
        sessionContexts.onLogon(aSession, fixDictionary);

        // corrupt buffer
        buffer.putBytes(8, new byte[1024]);

        newSessionContexts(buffer);

        verify(errorHandler).onError(any(FileSystemCorruptionException.class));
    }

    @Test(expected = IllegalArgumentException.class)
    public void validateSizeOfBuffer()
    {
        final AtomicBuffer buffer = new UnsafeBuffer(ByteBuffer.allocate(1024));
        newSessionContexts(buffer);
    }

    @Test
    public void wrapsOverSectorBoundaries()
    {
        final int requiredNumberOfWritesToSpanSector = 45;

        final List<CompositeKey> keys = IntStream
            .range(0, requiredNumberOfWritesToSpanSector)
            .mapToObj((i) -> idStrategy.onInitiateLogon("b" + i, null, null, "a" + i, null, null))
            .collect(toList());

        final List<SessionContext> contexts = keys
            .stream()
            .map(compositeKey -> sessionContexts.onLogon(compositeKey, fixDictionary))
            .peek(sessionContext -> sessionContext.onSequenceReset(time))
            .collect(toList());

        // Test an update of something not at the tail of the buffer.
        final SessionContext firstContext = contexts.get(0);
        firstContext.onSequenceReset(time);

        final SessionContexts contextsAfterRestart = newSessionContexts(buffer);
        IntStream
            .range(0, requiredNumberOfWritesToSpanSector)
            .forEach((i) -> assertValuesEqual(contexts.get(i),
            contextsAfterRestart.onLogon(keys.get(i), fixDictionary)));
    }

    @Test
    public void resetsSessionContexts()
    {
        final SessionContext aContext = sessionContexts.onLogon(aSession, fixDictionary);
        sessionContexts.onDisconnect(aContext.sessionId());

        sessionContexts.reset(null);

        assertSessionContextsReset(aContext, sessionContexts);

        verifyNoBackUp();
    }

    @Test
    public void resetsSessionContextsFile()
    {
        final SessionContext aContext = sessionContexts.onLogon(aSession, fixDictionary);
        sessionContexts.onDisconnect(aContext.sessionId());

        sessionContexts.reset(null);

        final SessionContexts sessionContextsAfterRestart = newSessionContexts(buffer);

        assertSessionContextsReset(aContext, sessionContextsAfterRestart);

        verifyNoBackUp();
    }

    @Test
    public void copiesOldSessionContextFile() throws IOException
    {
        final File backupLocation = File.createTempFile("sessionContexts", "tmp");
        try
        {
            final SessionContext aContext = sessionContexts.onLogon(aSession, fixDictionary);
            sessionContexts.onDisconnect(aContext.sessionId());

            final byte[] oldData = new byte[BUFFER_SIZE];
            buffer.getBytes(0, oldData);

            sessionContexts.reset(backupLocation);

            verify(mappedFile).transferTo(backupLocation);
        }
        finally
        {
            IoUtil.deleteIfExists(backupLocation);
        }
    }

    @Test
    public void doesNotReuseExistingSessionIdsForDistinctCompositeKeys()
    {
        sessionContexts.onLogon(aSession, fixDictionary);
        sessionContexts.onLogon(bSession, fixDictionary); // bump counter

        logonWithSenderAndTarget(aSession.localCompId(), aSession.remoteCompId());

        final SessionContext cContext = sessionContexts.onLogon(cSession, fixDictionary);

        assertNotEquals(DUPLICATE_SESSION, cContext);
        assertEquals(3, cContext.sessionId());
    }

    @Test
    public void shouldSupportDictionaryUpdatesAndCompaction()
    {
        final FixDictionary fixtDictionary = fixtDictionary();

        final SessionContext aContext = sessionContexts.onLogon(aSession, fixDictionary);
        sessionContexts.onLogon(bSession, fixDictionary);

        final long sessionIdA = aContext.sessionId();

        sessionContexts.onDisconnect(sessionIdA);
        final int filePosition1 = sessionContexts.filePosition();

        // Logon with new fix dictionary
        final SessionContext newAContext = sessionContexts.onLogon(aSession, fixtDictionary);
        assertEquals(fixtDictionary, newAContext.lastFixDictionary());
        final int filePosition2 = sessionContexts.filePosition();
        assertThat(filePosition2, greaterThan(filePosition1));

        // Restart with compaction
        sessionContexts = newSessionContexts(buffer);
        final SessionContext reloadedAContext = sessionContexts.lookupById(sessionIdA).getValue();
        assertEquals(fixtDictionary.getClass(), reloadedAContext.lastFixDictionary().getClass());
        final int filePosition3 = sessionContexts.filePosition();
        assertThat(filePosition3, lessThan(filePosition2));
    }

    @Test
    public void shouldReloadOldFileFormat() throws IOException
    {
        final InputStream oldFile = SessionContexts.class.getResourceAsStream("v2-session_id_buffer");
        final int size = EngineConfiguration.DEFAULT_SESSION_ID_BUFFER_SIZE;
        final byte[] data = new byte[size];
        oldFile.read(data);
        final UnsafeBuffer oldBuffer = new UnsafeBuffer(ByteBuffer.wrap(data));

        // Load old buffer
        final SessionContexts sessionContexts = newSessionContexts(oldBuffer);
        assertThat(sessionContexts.allSessions(), hasSize(1));

        // Modify contents of missing field
        final CompositeKey key = sessionContexts.allSessions().get(0).sessionKey();
        final FixDictionary fixtDictionary = fixtDictionary();
        final SessionContext context = sessionContexts.onLogon(key, fixtDictionary);
        assertEquals(fixtDictionary, context.lastFixDictionary());

        // Check that reloaded information is read
        final SessionContexts sessionContexts2 = newSessionContexts(oldBuffer);
        assertThat(sessionContexts2.allSessions(), hasSize(1));
        final SessionContext newContext = sessionContexts2.lookupById(context.sessionId()).getValue();
        assertEquals(fixtDictionary.getClass(), newContext.lastFixDictionary().getClass());
    }

    private FixDictionary fixtDictionary()
    {
        return FixDictionary.of(FixDictionaryImpl.class);
    }

    private void verifyNoBackUp()
    {
        verify(mappedFile, never()).transferTo(any());
    }

    private void assertSessionContextsReset(final SessionContext aContext, final SessionContexts sessionContexts)
    {
        final SessionContext bContext = sessionContexts.onLogon(bSession, fixDictionary);
        final SessionContext newAContext = sessionContexts.onLogon(aSession, fixDictionary);
        assertValidSessionId(bContext.sessionId());
        assertValidSessionId(newAContext.sessionId());
        assertEquals("Session Contexts haven't been reset", aContext, bContext);
        assertNotEquals("Session Contexts haven't been reset", aContext, newAContext);
    }

    private void assertValidSessionId(final long cId)
    {
        assertThat(cId, greaterThanOrEqualTo(LOWEST_VALID_SESSION_ID));
    }

    private SessionContexts newSessionContexts(final AtomicBuffer buffer)
    {
        return newSessionContexts(buffer, DEFAULT_INITIAL_SEQUENCE_INDEX);
    }

    private SessionContexts newSessionContexts(final AtomicBuffer buffer, final int initialSequenceIndex)
    {
        when(mappedFile.buffer()).thenReturn(buffer);
        return new SessionContexts(mappedFile, idStrategy, initialSequenceIndex, errorHandler);
    }

    private void assertValuesEqual(
        final SessionContext sessionContext,
        final SessionContext secondSessionContext)
    {
        assertEquals(sessionContext, secondSessionContext);
        assertEquals(sessionContext.sequenceIndex(), secondSessionContext.sequenceIndex());
    }

    private long logonWithSenderAndTarget(final String senderCompID, final String targetCompID)
    {
        logonEncoder.header()
            .sendingTime(new byte[] {0})
            .senderCompID(senderCompID)
            .targetCompID(targetCompID);
        return logonEncoder.encryptMethod(0).heartBtInt(0).encode(asciiBuffer, 0);
    }
}