/*
 * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
 */

package at.bitfire.davdroid.resource

import android.Manifest
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.Context
import android.provider.ContactsContract
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.LabeledProperty
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import ezvcard.property.Telephone
import org.junit.AfterClass
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import java.io.FileNotFoundException
import java.util.LinkedList
import java.util.Optional
import javax.inject.Inject

@HiltAndroidTest
class LocalAddressBookTest {

    @Inject @ApplicationContext
    lateinit var context: Context

    @Inject
    lateinit var localTestAddressBookProvider: LocalTestAddressBookProvider

    @get:Rule
    val hiltRule = HiltAndroidRule(this)

    val account = Account("Test Account", "Test Account Type")

    @Before
    fun setUp() {
        hiltRule.inject()
    }


    /**
     * Tests whether contacts are moved (and not lost) when an address book is renamed.
     */
    @Test
    fun test_renameAccount_retainsContacts() {
        localTestAddressBookProvider.provide(account, provider) { addressBook ->
            // insert contact with data row
            val uid = "12345"
            val contact = Contact(
                uid = uid,
                displayName = "Test Contact",
                phoneNumbers = LinkedList(listOf(LabeledProperty(Telephone("1234567890"))))
            )
            val uri = LocalContact(addressBook, contact, null, null, 0).add()
            val id = ContentUris.parseId(uri)
            val localContact = addressBook.findContactById(id)
            localContact.resetDirty()
            assertFalse("Contact is dirty before moving", isContactDirty(addressBook, id))

            // rename address book
            val newName = "New Name"
            addressBook.renameAccount(newName)
            assertEquals(newName, addressBook.addressBookAccount.name)

            // check whether contact is still here (including data rows) and not dirty
            val result = addressBook.findContactById(id)
            assertFalse("Contact is dirty after moving", isContactDirty(addressBook, id))

            val contact2 = result.getContact()
            assertEquals(uid, contact2.uid)
            assertEquals("Test Contact", contact2.displayName)
            assertEquals("1234567890", contact2.phoneNumbers.first().component1().text)
        }
    }

    /**
     * Tests whether groups are moved (and not lost) when an address book is renamed.
     */
    @Test
    fun test_renameAccount_retainsGroups() {
        localTestAddressBookProvider.provide(account, provider) { addressBook ->
            // insert group
            val localGroup = LocalGroup(addressBook, Contact(displayName = "Test Group"), null, null, 0)
            val uri = localGroup.add()
            val id = ContentUris.parseId(uri)

            // make sure it's not dirty
            localGroup.clearDirty(Optional.empty(), null, null)
            assertFalse("Group is dirty before moving", isGroupDirty(addressBook, id))

            // rename address book
            val newName = "New Name"
            assertTrue(addressBook.renameAccount(newName))
            assertEquals(newName, addressBook.addressBookAccount.name)

            // check whether group is still here and not dirty
            val result = addressBook.findGroupById(id)
            assertFalse("Group is dirty after moving", isGroupDirty(addressBook, id))

            val group = result.getContact()
            assertEquals("Test Group", group.displayName)
        }
    }


    // helpers

    /**
     * Returns the dirty flag of the given contact.
     *
     * @return true if the contact is dirty, false otherwise
     *
     * @throws FileNotFoundException if the contact can't be found
     */
    fun isContactDirty(adddressBook: LocalAddressBook, id: Long): Boolean {
        val uri = ContentUris.withAppendedId(adddressBook.rawContactsSyncUri(), id)
        provider.query(uri, arrayOf(ContactsContract.RawContacts.DIRTY), null, null, null)?.use { cursor ->
            if (cursor.moveToFirst())
                return cursor.getInt(0) != 0
        }
        throw FileNotFoundException()
    }

    /**
     * Returns the dirty flag of the given contact group.
     *
     * @return true if the group is dirty, false otherwise
     *
     * @throws FileNotFoundException if the group can't be found
     */
    fun isGroupDirty(adddressBook: LocalAddressBook, id: Long): Boolean {
        val uri = ContentUris.withAppendedId(adddressBook.groupsSyncUri(), id)
        provider.query(uri, arrayOf(ContactsContract.Groups.DIRTY), null, null, null)?.use { cursor ->
            if (cursor.moveToFirst())
                return cursor.getInt(0) != 0
        }
        throw FileNotFoundException()
    }


    companion object {

        @JvmField
        @ClassRule
        val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!

        private lateinit var provider: ContentProviderClient

        @BeforeClass
        @JvmStatic
        fun connect() {
            val context = InstrumentationRegistry.getInstrumentation().context
            provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
        }

        @AfterClass
        @JvmStatic
        fun disconnect() {
            provider.close()
        }
    }

}