package com.doist.jobschedulercompat.scheduler.alarm;

import com.doist.jobschedulercompat.JobInfo;
import com.doist.jobschedulercompat.job.JobStatus;
import com.doist.jobschedulercompat.job.JobStore;
import com.doist.jobschedulercompat.util.DeviceTestUtils;
import com.doist.jobschedulercompat.util.JobCreator;
import com.doist.jobschedulercompat.util.ShadowNetworkInfo;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ServiceController;
import org.robolectric.annotation.Config;

import android.app.Application;
import android.net.Uri;
import android.os.Build;

import java.util.concurrent.TimeUnit;

import androidx.test.core.app.ApplicationProvider;

import static org.junit.Assert.assertEquals;
import static org.robolectric.Shadows.shadowOf;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = Build.VERSION_CODES.KITKAT, shadows = {ShadowNetworkInfo.class})
public class AlarmJobServiceTest {
    private static final long DELAY_MS = 500;
    private static final long LATENCY_MS = TimeUnit.HOURS.toMillis(1);

    private Application application;
    private JobStore jobStore;
    private ServiceController<AlarmJobService> service;

    @Before
    public void setup() {
        application = ApplicationProvider.getApplicationContext();
        jobStore = JobStore.get(application);
        service = Robolectric.buildService(AlarmJobService.class).create();
    }

    @After
    public void teardown() {
        JobCreator.interruptJobs();
        synchronized (JobStore.LOCK) {
            jobStore.clear();
        }
    }

    @Test
    public void testJobRuns() {
        DeviceTestUtils.setDeviceIdle(application, true);
        JobInfo job = JobCreator.create(application, DELAY_MS).setRequiresStorageNotLow(true).build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(1);
    }

    @Test
    public void testJobFinishes() {
        DeviceTestUtils.setCharging(application, true);
        JobInfo job = JobCreator.create(application, 50).setRequiresCharging(true).build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(1);

        JobCreator.waitForJob(job.getId());

        assertBoundServiceCount(0);
    }

    @Test
    public void testChargingConstraint() {
        DeviceTestUtils.setCharging(application, false);
        JobInfo job = JobCreator.create(application, DELAY_MS).setRequiresCharging(true).build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(0);

        DeviceTestUtils.setCharging(application, true);
        service.startCommand(0, 0);

        assertBoundServiceCount(1);
    }

    @Test
    public void testIdleConstraint() {
        DeviceTestUtils.setDeviceIdle(application, false);
        JobInfo job = JobCreator.create(application, DELAY_MS).setRequiresDeviceIdle(true).build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(0);

        DeviceTestUtils.setDeviceIdle(application, true);
        service.startCommand(0, 0);

        assertBoundServiceCount(1);
    }

    @Test
    public void testConnectivityConstraint() {
        DeviceTestUtils.setNetworkInfo(application, false, false, false);
        JobInfo job = JobCreator.create(application, DELAY_MS).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY).build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(0);

        DeviceTestUtils.setNetworkInfo(application, true, false, false);
        service.startCommand(0, 0);

        assertBoundServiceCount(1);
    }

    @Test
    public void testNotRoamingConstraint() {
        DeviceTestUtils.setNetworkInfo(application, true, true, false);
        JobInfo job = JobCreator.create(application, DELAY_MS)
                                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING)
                                .build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(0);

        DeviceTestUtils.setNetworkInfo(application, true, false, false);
        service.startCommand(0, 0);

        assertBoundServiceCount(1);
    }

    @Test
    public void testUnmeteredConstraint() {
        DeviceTestUtils.setNetworkInfo(application, true, false, false);
        JobInfo job = JobCreator.create(application, DELAY_MS)
                                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
                                .build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(0);

        DeviceTestUtils.setNetworkInfo(application, true, false, true);
        service.startCommand(0, 0);

        assertBoundServiceCount(1);
    }

    @Test
    public void testContentTriggerConstraint() {
        JobInfo job = JobCreator.create(application, DELAY_MS)
                                .addTriggerContentUri(new JobInfo.TriggerContentUri(Uri.parse("com.doist"), 0))
                                .build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));

        service.startCommand(0, 0);

        assertEquals(ContentObserverService.class.getCanonicalName(),
                     shadowOf(application).getNextStartedService().getComponent().getClassName());
    }

    @Test
    public void testLatencyConstraint() {
        JobInfo job = JobCreator.create(application, DELAY_MS).setMinimumLatency(LATENCY_MS).build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(0);

        DeviceTestUtils.advanceTime(LATENCY_MS);
        service.startCommand(0, 0);

        assertBoundServiceCount(1);
    }

    @Test
    public void testDeadlineConstraint() {
        JobInfo job = JobCreator.create(application, DELAY_MS)
                                .setRequiresCharging(true)
                                .setOverrideDeadline(LATENCY_MS)
                                .build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(0);

        DeviceTestUtils.advanceTime(LATENCY_MS);
        service.startCommand(0, 0);

        assertBoundServiceCount(1);
    }

    @Test
    public void testMixedConstraints() {
        DeviceTestUtils.setCharging(application, false);
        DeviceTestUtils.setDeviceIdle(application, false);
        DeviceTestUtils.setNetworkInfo(application, false, false, false);

        JobInfo job = JobCreator.create(application, DELAY_MS)
                                .setRequiresCharging(true)
                                .setRequiresDeviceIdle(true)
                                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
                                .setMinimumLatency(LATENCY_MS)
                                .build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(0);

        DeviceTestUtils.setCharging(application, true);
        service.startCommand(0, 0);

        assertBoundServiceCount(0);

        DeviceTestUtils.setDeviceIdle(application, true);
        service.startCommand(0, 0);

        assertBoundServiceCount(0);

        DeviceTestUtils.setNetworkInfo(application, true, false, true);
        service.startCommand(0, 0);

        assertBoundServiceCount(0);

        DeviceTestUtils.advanceTime(LATENCY_MS);
        service.startCommand(0, 0);

        assertBoundServiceCount(1);
    }

    @Test
    public void testRemoveRunningJob() {
        JobInfo job = JobCreator.create(application, DELAY_MS).setRequiresStorageNotLow(true).build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        job = JobCreator.create(application, DELAY_MS).setRequiresStorageNotLow(true).build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(2);

        jobStore.remove(job.getId());
        service.startCommand(0, 0);

        assertBoundServiceCount(1);
    }

    @Test
    public void testRemoveAllRunningJobs() {
        JobInfo job = JobCreator.create(application, DELAY_MS).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY).build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        job = JobCreator.create(application, DELAY_MS).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY).build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(2);

        jobStore.clear();
        service.startCommand(0, 0);

        assertBoundServiceCount(0);
    }

    @Test
    public void testConstraintUnmetWhileRunningJob() {
        DeviceTestUtils.setCharging(application, true);
        JobInfo job = JobCreator.create(application, DELAY_MS).setRequiresCharging(true).build();
        jobStore.add(JobStatus.createFromJobInfo(job, AlarmScheduler.TAG));
        service.startCommand(0, 0);

        assertBoundServiceCount(1);

        DeviceTestUtils.setCharging(application, false);
        service.startCommand(0, 0);

        assertBoundServiceCount(0);
    }

    private void assertBoundServiceCount(int count) {
        assertEquals(count, shadowOf(application).getBoundServiceConnections().size());
    }
}