package com.doist.jobschedulercompat.scheduler.gcm;

import com.google.android.gms.common.ConnectionResult;

import com.doist.jobschedulercompat.JobInfo;
import com.doist.jobschedulercompat.PersistableBundle;
import com.doist.jobschedulercompat.util.JobCreator;
import com.doist.jobschedulercompat.util.ShadowGoogleApiAvailability;

import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import android.app.Application;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;

import java.util.List;
import java.util.concurrent.TimeUnit;

import androidx.test.core.app.ApplicationProvider;

import static com.doist.jobschedulercompat.scheduler.gcm.GcmScheduler.PARAM_REQUIRES_CHARGING;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.robolectric.Shadows.shadowOf;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = Build.VERSION_CODES.KITKAT, shadows = {ShadowGoogleApiAvailability.class})
public class GcmSchedulerTest {
    private Application application;
    private GcmScheduler scheduler;

    @BeforeClass
    public static void enableGcm() {
        ShadowGoogleApiAvailability.setIsGooglePlayServicesAvailable(ConnectionResult.SUCCESS);
    }

    @Before
    public void setup() {
        application = ApplicationProvider.getApplicationContext();
        scheduler = new GcmScheduler(application);
    }

    @Test
    public void testScheduleBroadcasts() {
        JobInfo job = JobCreator.create(application)
                                .setMinimumLatency(TimeUnit.HOURS.toMillis(2))
                                .setOverrideDeadline(TimeUnit.DAYS.toMillis(1))
                                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                                .setPersisted(true)
                                .build();
        scheduler.schedule(job);

        Intent intent = getLastBroadcastIntent();
        assertNotNull(intent);
        assertIntentMatchesJobInfo(intent, job);

        PersistableBundle extras = new PersistableBundle();
        extras.putString("test", "test");
        job = JobCreator.create(application)
                        .setExtras(extras)
                        .setPeriodic(TimeUnit.MINUTES.toMillis(30))
                        .setRequiresCharging(true)
                        .setRequiresDeviceIdle(true)
                        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
                        .build();
        scheduler.schedule(job);

        intent = getLastBroadcastIntent();
        assertNotNull(intent);
        assertIntentMatchesJobInfo(intent, job);

        job = JobCreator.create(application)
                        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING)
                        .addTriggerContentUri(new JobInfo.TriggerContentUri(Uri.parse("doist.com"), 0))
                        .addTriggerContentUri(new JobInfo.TriggerContentUri(
                                Uri.parse("todoist.com"), JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS))
                        .addTriggerContentUri(new JobInfo.TriggerContentUri(Uri.parse("twist.com"), 0))
                        .build();
        scheduler.schedule(job);

        intent = getLastBroadcastIntent();
        assertNotNull(intent);
        assertIntentMatchesJobInfo(intent, job);
    }

    @Test
    public void testCancelBroadcasts() {
        scheduler.cancel(0);

        Intent intent = getLastBroadcastIntent();
        assertNotNull(intent);
        assertEquals(GcmScheduler.ACTION_SCHEDULE, intent.getAction());
        assertEquals(GcmScheduler.PACKAGE_GMS, intent.getPackage());
        assertEquals(GcmScheduler.SCHEDULER_ACTION_CANCEL_TASK,
                     intent.getStringExtra(GcmScheduler.BUNDLE_PARAM_SCHEDULER_ACTION));
        assertThat(intent.getParcelableExtra(GcmScheduler.BUNDLE_PARAM_TOKEN), instanceOf(PendingIntent.class));
        ComponentName component = intent.getParcelableExtra(GcmScheduler.PARAM_COMPONENT);
        assertNotNull(component);
        assertEquals(GcmJobService.class.getName(), component.getClassName());
    }

    @Test
    public void testCancelAllBroadcasts() {
        scheduler.cancelAll();

        Intent intent = getLastBroadcastIntent();
        assertNotNull(intent);
        assertEquals(GcmScheduler.ACTION_SCHEDULE, intent.getAction());
        assertEquals(GcmScheduler.PACKAGE_GMS, intent.getPackage());
        assertEquals(GcmScheduler.SCHEDULER_ACTION_CANCEL_ALL,
                     intent.getStringExtra(GcmScheduler.BUNDLE_PARAM_SCHEDULER_ACTION));
        assertThat(intent.getParcelableExtra(GcmScheduler.BUNDLE_PARAM_TOKEN),
                   instanceOf(PendingIntent.class));
        ComponentName component = intent.getParcelableExtra(GcmScheduler.PARAM_COMPONENT);
        assertNotNull(component);
        assertEquals(GcmJobService.class.getName(), component.getClassName());
    }

    private void assertIntentMatchesJobInfo(Intent intent, JobInfo job) {
        assertEquals(GcmScheduler.ACTION_SCHEDULE, intent.getAction());
        assertEquals(GcmScheduler.PACKAGE_GMS, intent.getPackage());
        assertEquals(GcmScheduler.SCHEDULER_ACTION_SCHEDULE_TASK,
                     intent.getStringExtra(GcmScheduler.BUNDLE_PARAM_SCHEDULER_ACTION));
        assertThat(intent.getParcelableExtra(GcmScheduler.BUNDLE_PARAM_TOKEN), instanceOf(PendingIntent.class));

        assertEquals(job.getId(), (int) Integer.valueOf(intent.getStringExtra(GcmScheduler.PARAM_TAG)));
        assertEquals(GcmJobService.class.getName(), intent.getStringExtra(GcmScheduler.PARAM_SERVICE));
        assertTrue(intent.getBooleanExtra(GcmScheduler.PARAM_UPDATE_CURRENT, false));
        assertEquals(job.isPersisted(), intent.getBooleanExtra(GcmScheduler.PARAM_PERSISTED, false));

        if (job.hasEarlyConstraint() || job.hasLateConstraint()) {
            assertEquals(GcmScheduler.TRIGGER_TYPE_EXECUTION_WINDOW,
                         intent.getIntExtra(GcmScheduler.PARAM_TRIGGER_TYPE, -1));
            if (job.isPeriodic()) {
                assertEquals(TimeUnit.MILLISECONDS.toSeconds(job.getIntervalMillis()),
                             intent.getLongExtra(GcmScheduler.PARAM_TRIGGER_WINDOW_PERIOD, -1L));
                assertEquals(TimeUnit.MILLISECONDS.toSeconds(job.getIntervalMillis()),
                             intent.getLongExtra(GcmScheduler.PARAM_TRIGGER_WINDOW_FLEX, -1L));
            } else {
                assertEquals(TimeUnit.MILLISECONDS.toSeconds(job.getMinLatencyMillis()),
                             intent.getLongExtra(GcmScheduler.PARAM_TRIGGER_WINDOW_START, -1L));
                if (job.hasLateConstraint()) {
                    assertEquals(TimeUnit.MILLISECONDS.toSeconds(job.getMaxExecutionDelayMillis()),
                                 intent.getLongExtra(GcmScheduler.PARAM_TRIGGER_WINDOW_END, -1L));
                } else {
                    assertEquals(TimeUnit.DAYS.toSeconds(7),
                                 intent.getLongExtra(GcmScheduler.PARAM_TRIGGER_WINDOW_END, -1L));
                }
            }
        } else if (job.getTriggerContentUris() != null) {
            JobInfo.TriggerContentUri[] triggerContentUris = job.getTriggerContentUris();
            int size = triggerContentUris.length;
            Uri[] uriArray = new Uri[size];
            int[] flagsArray = new int[size];
            for (int i = 0; i < size; i++) {
                JobInfo.TriggerContentUri triggerContentUri = triggerContentUris[i];
                uriArray[i] = triggerContentUri.getUri();
                flagsArray[i] = triggerContentUri.getFlags();
            }
            assertEquals(GcmScheduler.TRIGGER_TYPE_CONTENT_URI,
                         intent.getIntExtra(GcmScheduler.PARAM_TRIGGER_TYPE, -1));
            assertArrayEquals(uriArray, intent.getParcelableArrayExtra(GcmScheduler.PARAM_CONTENT_URI_ARRAY));
            assertArrayEquals(flagsArray, intent.getIntArrayExtra(GcmScheduler.PARAM_CONTENT_URI_FLAGS_ARRAY));
        } else {
            assertEquals(GcmScheduler.TRIGGER_TYPE_IMMEDIATE,
                         intent.getIntExtra(GcmScheduler.PARAM_TRIGGER_TYPE, -1));
            assertEquals(0L, intent.getLongExtra(GcmScheduler.PARAM_TRIGGER_WINDOW_START, -1L));
            assertEquals(1L, intent.getLongExtra(GcmScheduler.PARAM_TRIGGER_WINDOW_END, -1L));
        }

        assertEquals(job.isRequireCharging(), intent.getBooleanExtra(PARAM_REQUIRES_CHARGING, false));
        int requiredNetwork;
        switch (job.getNetworkType()) {
            case JobInfo.NETWORK_TYPE_ANY:
                requiredNetwork = GcmScheduler.NETWORK_STATE_CONNECTED;
                break;

            case JobInfo.NETWORK_TYPE_UNMETERED:
                requiredNetwork = GcmScheduler.NETWORK_STATE_UNMETERED;
                break;

            default:
                requiredNetwork = GcmScheduler.NETWORK_STATE_ANY;
                break;
        }
        assertEquals(requiredNetwork, intent.getIntExtra(GcmScheduler.PARAM_REQUIRED_NETWORK, -1));

        Bundle retryStrategy = intent.getBundleExtra(GcmScheduler.PARAM_RETRY_STRATEGY);
        int backoffPolicy = job.getBackoffPolicy() == JobInfo.BACKOFF_POLICY_LINEAR
                            ? GcmScheduler.RETRY_POLICY_LINEAR : GcmScheduler.RETRY_POLICY_EXPONENTIAL;
        assertEquals(backoffPolicy, retryStrategy.getInt(GcmScheduler.PARAM_RETRY_STRATEGY_POLICY, -1));
        assertEquals(TimeUnit.MILLISECONDS.toSeconds(job.getInitialBackoffMillis()),
                     retryStrategy.getInt(GcmScheduler.PARAM_RETRY_STRATEGY_INITIAL_BACKOFF_SEC, -1));
        assertEquals(TimeUnit.HOURS.toSeconds(5),
                     retryStrategy.getInt(GcmScheduler.PARAM_RETRY_STRATEGY_MAXIMUM_BACKOFF_SEC, -1));

        assertEquals(job.getExtras().toMap(10),
                     new PersistableBundle(intent.getBundleExtra(GcmScheduler.PARAM_EXTRAS)).toMap(10));
    }

    private Intent getLastBroadcastIntent() {
        List<Intent> broadcastIntents = shadowOf(application).getBroadcastIntents();
        int count = broadcastIntents.size();
        if (count > 0) {
            return broadcastIntents.get(count - 1);
        } else {
            return null;
        }
    }
}