package ch.threema.app.testutils;

import android.util.Log;

import ch.threema.base.crypto.NaCl;

import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.Until;
import ch.threema.app.managers.ServiceManager;
import ch.threema.app.services.UserService;
import ch.threema.domain.helpers.InMemoryIdentityStore;
import ch.threema.domain.models.Contact;
import ch.threema.domain.models.BasicContact;
import ch.threema.domain.models.GroupId;
import ch.threema.domain.models.IdentityState;
import ch.threema.domain.models.IdentityType;
import ch.threema.domain.models.UserState;
import ch.threema.domain.models.VerificationLevel;
import ch.threema.domain.models.WorkVerificationLevel;
import ch.threema.domain.protocol.ThreemaFeature;
import ch.threema.domain.stores.IdentityStore;
import ch.threema.storage.models.ContactModel;
import ch.threema.storage.models.group.GroupModelOld;

import static org.junit.Assert.assertNotNull;
import static ch.threema.common.JavaCompat.hexToByteArray;

public class TestHelpers {
    private static final String TAG = "TestHelpers";

    public static final TestContact TEST_CONTACT = new TestContact(
        "XERCUKNS",
        hexToByteArray("2bbc16092ff45ffcd0045c00f2f5e1e9597621f89360bbca23a2a2956b3c3b36"),
        hexToByteArray("977aba4ab367041f6137afef69ab9676d445011ca7aca0455a5c64805b80b77a")
    );

    public static final class TestContact {
        @NonNull
        public final String identity;
        @NonNull
        public final byte[] publicKey;
        @NonNull
        public final byte[] privateKey;

        public TestContact(@NonNull String identity) {
            this.identity = identity;
            publicKey = new byte[NaCl.PUBLIC_KEY_BYTES];
            privateKey = new byte[NaCl.SECRET_KEY_BYTES];

            NaCl.generateKeypairInPlace(publicKey, privateKey);
        }

        public TestContact(@NonNull String identity, @NonNull byte[] publicKey, @NonNull byte[] privateKey) {
            this.identity = identity;
            this.publicKey = publicKey;
            this.privateKey = privateKey;
        }

        @NonNull
        public Contact getContact() {
            return new Contact(this.identity, this.publicKey, VerificationLevel.UNVERIFIED);
        }

        @NonNull
        public ContactModel getContactModel() {
            return ContactModel.create(this.identity, this.publicKey);
        }

        @NonNull
        public IdentityStore getIdentityStore() {
            return new InMemoryIdentityStore(
                this.identity,
                "",
                this.privateKey,
                null
            );
        }

        @NonNull
        public BasicContact toBasicContact() {
            return BasicContact.javaCreate(
                identity,
                publicKey,
                new ThreemaFeature.Builder()
                    .audio(true)
                    .group(true)
                    .poll(true)
                    .file(true)
                    .voip(true)
                    .videocalls(true)
                    .forwardSecurity(true)
                    .groupCalls(true)
                    .editMessages(true)
                    .deleteMessages(true)
                    .build(),
                IdentityState.ACTIVE,
                IdentityType.REGULAR,
                VerificationLevel.UNVERIFIED,
                WorkVerificationLevel.NONE,
                null,
                null
            );
        }
    }

    public static final class TestGroup {
        private int localGroupId = -1;

        @NonNull
        public final GroupId apiGroupId;

        @NonNull
        public final TestContact groupCreator;

        @NonNull
        public final List<TestContact> members;

        @NonNull
        public final String groupName;

        @Nullable
        public final byte[] profilePicture;

        /**
         * Note that the user identity is used to set the correct group user state.
         */
        @NonNull
        public final String userIdentity;

        public TestGroup(
            @NonNull GroupId apiGroupId,
            @NonNull TestContact groupCreator,
            @NonNull List<TestContact> members,
            @NonNull String groupName,
            @NonNull String userIdentity
        ) {
            this(apiGroupId, groupCreator, members, groupName, null, userIdentity);
        }

        public TestGroup(
            @NonNull GroupId apiGroupId,
            @NonNull TestContact groupCreator,
            @NonNull List<TestContact> members,
            @NonNull String groupName,
            @Nullable byte[] profilePicture,
            @NonNull String userIdentity
        ) {
            this.apiGroupId = apiGroupId;
            this.groupCreator = groupCreator;
            this.members = members;
            this.groupName = groupName;
            this.profilePicture = profilePicture;
            this.userIdentity = userIdentity;
        }

        @NonNull
        public GroupModelOld getGroupModel() {
            boolean isMember = false;
            for (TestContact member : members) {
                if (member.identity.equals(userIdentity)) {
                    isMember = true;
                    break;
                }
            }
            return getGroupModel(isMember ? UserState.MEMBER : UserState.LEFT);
        }

        @NonNull
        private GroupModelOld getGroupModel(@NonNull UserState userState) {
            return new GroupModelOld()
                .setApiGroupId(apiGroupId)
                .setCreatedAt(Instant.now())
                .setName(this.groupName)
                .setCreatorIdentity(this.groupCreator.identity)
                .setId(localGroupId)
                .setUserState(userState);
        }

        @NonNull
        public Collection<TestContact> getMembersWithoutCreator() {
            return members.stream()
                .filter(member -> !member.identity.equals(groupCreator.identity))
                .collect(Collectors.toList());
        }

        public void setLocalGroupId(int localGroupId) {
            this.localGroupId = localGroupId;
        }
    }

    /**
     * Open the notification area and wait for the notifications to become visible.
     *
     * @param device UiDevice instance
     */
    public static void openNotificationArea(@NonNull UiDevice device) throws AssertionError {
        device.openNotification();

        // Wait for notifications to appear
        final BySelector selector = By.res("android:id/status_bar_latest_event_content");
        assertNotNull(
            "Notification bar latest event content not found",
            device.wait(Until.findObject(selector), 1000)
        );
    }

    /**
     * Set the provided identity if the current identity is not set or different.
     */
    public static void setIdentity(@NonNull ServiceManager serviceManager, TestContact user) throws Exception {
        UserService userService = serviceManager.getUserService();
        if (userService.hasIdentity() && userService.getIdentity().equals(user.identity)) {
            return;
        }

        userService.restoreIdentity(
            user.identity,
            user.privateKey
        );
    }

    /**
     * Ensure that an identity is set up.
     */
    public static String ensureIdentity(@NonNull ServiceManager serviceManager) throws Exception {
        // Check whether identity already exists
        final UserService userService = serviceManager.getUserService();
        if (userService.hasIdentity()) {
            final String identity = userService.getIdentity();
            Log.i(TAG, "Identity already exists: " + identity);
            return identity;
        }

        // Otherwise, create identity
        userService.restoreIdentity(
            TEST_CONTACT.identity,
            TEST_CONTACT.privateKey
        );
        Log.i(TAG, "Test identity restored: " + TEST_CONTACT.identity);
        return TEST_CONTACT.identity;
    }
}
