/*
 * Copyright 2019 New Vector 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
 *
 * 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 im.vector.matrix.android.session.room.timeline

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.helper.addTimelineEvent
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.createObject
import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldEqual
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
internal class ChunkEntityTest : InstrumentedTest {

    private lateinit var monarchy: Monarchy

    @Before
    fun setup() {
        Realm.init(context())
        val testConfig = RealmConfiguration.Builder()
                .inMemory()
                .name("test-realm")
                .modules(SessionRealmModule())
                .build()
        monarchy = Monarchy.Builder().setRealmConfiguration(testConfig).build()
    }

    @Test
    fun add_shouldAdd_whenNotAlreadyIncluded() {
        monarchy.runTransactionSync { realm ->
            val chunk: ChunkEntity = realm.createObject()

            val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let {
                realm.copyToRealmOrUpdate(it)
            }
            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
            chunk.timelineEvents.size shouldEqual 1
        }
    }

    @Test
    fun add_shouldNotAdd_whenAlreadyIncluded() {
        monarchy.runTransactionSync { realm ->
            val chunk: ChunkEntity = realm.createObject()
            val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let {
                realm.copyToRealmOrUpdate(it)
            }
            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
            chunk.timelineEvents.size shouldEqual 1
        }
    }

    @Test
    fun merge_shouldAddEvents_whenMergingBackward() {
        monarchy.runTransactionSync { realm ->
            val chunk1: ChunkEntity = realm.createObject()
            val chunk2: ChunkEntity = realm.createObject()
            chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
            chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
            chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
            chunk1.timelineEvents.size shouldEqual 60
        }
    }

    @Test
    fun merge_shouldAddOnlyDifferentEvents_whenMergingBackward() {
        monarchy.runTransactionSync { realm ->
            val chunk1: ChunkEntity = realm.createObject()
            val chunk2: ChunkEntity = realm.createObject()
            val eventsForChunk1 = createFakeListOfEvents(30)
            val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
            chunk1.isLastForward = true
            chunk2.isLastForward = false
            chunk1.addAll(ROOM_ID, eventsForChunk1, PaginationDirection.FORWARDS)
            chunk2.addAll(ROOM_ID, eventsForChunk2, PaginationDirection.BACKWARDS)
            chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
            chunk1.timelineEvents.size shouldEqual 40
            chunk1.isLastForward.shouldBeTrue()
        }
    }

    @Test
    fun merge_shouldPrevTokenMerged_whenMergingForwards() {
        monarchy.runTransactionSync { realm ->
            val chunk1: ChunkEntity = realm.createObject()
            val chunk2: ChunkEntity = realm.createObject()
            val prevToken = "prev_token"
            chunk1.prevToken = prevToken
            chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
            chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
            chunk1.merge(ROOM_ID, chunk2, PaginationDirection.FORWARDS)
            chunk1.prevToken shouldEqual prevToken
        }
    }

    @Test
    fun merge_shouldNextTokenMerged_whenMergingBackwards() {
        monarchy.runTransactionSync { realm ->
            val chunk1: ChunkEntity = realm.createObject()
            val chunk2: ChunkEntity = realm.createObject()
            val nextToken = "next_token"
            chunk1.nextToken = nextToken
            chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
            chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
            chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
            chunk1.nextToken shouldEqual nextToken
        }
    }

    private fun ChunkEntity.addAll(roomId: String,
                                   events: List<Event>,
                                   direction: PaginationDirection) {
        events.forEach { event ->
            val fakeEvent = event.toEntity(roomId, SendState.SYNCED).let {
                realm.copyToRealmOrUpdate(it)
            }
            addTimelineEvent(roomId, fakeEvent, direction, emptyMap())
        }
    }

    companion object {
        private const val ROOM_ID = "roomId"
    }
}
