package com.android.server.notification;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;

import android.app.Application;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.service.notification.Condition;
import android.service.notification.ScheduleCalendar;
import android.service.notification.ZenModeConfig;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;

import androidx.test.filters.SmallTest;

import com.android.server.UiServiceTestCase;
import com.android.server.pm.PackageManagerService;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;

import java.util.Calendar;
import java.util.GregorianCalendar;

@RunWith(AndroidTestingRunner.class)
@SmallTest
@RunWithLooper
public class ScheduleConditionProviderTest extends UiServiceTestCase {

    ScheduleConditionProvider mService;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        Intent startIntent =
                new Intent("com.android.server.notification.ScheduleConditionProvider");
        startIntent.setPackage("android");
        ScheduleConditionProvider service = new ScheduleConditionProvider();
        service.attach(
                getContext(),
                null,               // ActivityThread not actually used in Service
                ScheduleConditionProvider.class.getName(),
                null,               // token not needed when not talking with the activity manager
                mock(Application.class),
                null                // mocked services don't talk with the activity manager
                );
        service.onCreate();
        service.onBind(startIntent);
        mService = spy(service);
   }

    @Test
    public void testIsValidConditionId_incomplete() throws Exception {
        Uri badConditionId = Uri.EMPTY;
        assertFalse(mService.isValidConditionId(badConditionId));
        assertEquals(Condition.STATE_ERROR,
                mService.evaluateSubscriptionLocked(badConditionId, null, 0, 1000).state);
    }

    @Test
    public void testIsValidConditionId() throws Exception {
        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
        info.days = new int[] {1, 2, 4};
        info.startHour = 8;
        info.startMinute = 56;
        info.nextAlarm = 1000;
        info.exitAtAlarm = true;
        info.endHour = 12;
        info.endMinute = 9;
        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
        assertTrue(mService.isValidConditionId(conditionId));
    }

    @Test
    public void testEvaluateSubscription_noAlarmExit_InSchedule() {
        Calendar now = getNow();

        // Schedule - 1 hour long; starts now
        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
        info.days = new int[] {Calendar.FRIDAY};
        info.startHour = now.get(Calendar.HOUR_OF_DAY);
        info.startMinute = now.get(Calendar.MINUTE);
        info.nextAlarm = 0;
        info.exitAtAlarm = false;
        info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
        info.endMinute = info.startMinute;
        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
        ScheduleCalendar cal = new ScheduleCalendar();
        cal.setSchedule(info);
        assertTrue(cal.isInSchedule(now.getTimeInMillis()));

        Condition condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);

        assertEquals(Condition.STATE_TRUE, condition.state);
    }

    @Test
    public void testEvaluateSubscription_noAlarmExit_InScheduleSnoozed() {
        Calendar now = getNow();

        // Schedule - 1 hour long; starts now
        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
        info.days = new int[] {Calendar.FRIDAY};
        info.startHour = now.get(Calendar.HOUR_OF_DAY);
        info.startMinute = now.get(Calendar.MINUTE);
        info.nextAlarm = 0;
        info.exitAtAlarm = false;
        info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
        info.endMinute = info.startMinute;
        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
        ScheduleCalendar cal = new ScheduleCalendar();
        cal.setSchedule(info);
        assertTrue(cal.isInSchedule(now.getTimeInMillis()));

        mService.addSnoozed(conditionId);

        Condition condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);

        assertEquals(Condition.STATE_FALSE, condition.state);
    }

    @Test
    public void testEvaluateSubscription_noAlarmExit_beforeSchedule() {
        Calendar now = new GregorianCalendar();
        now.set(Calendar.HOUR_OF_DAY, 14);
        now.set(Calendar.MINUTE, 15);
        now.set(Calendar.SECOND, 59);
        now.set(Calendar.MILLISECOND, 0);
        now.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);

        // Schedule - 1 hour long; starts in 1 second
        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
        info.days = new int[] {Calendar.FRIDAY};
        info.startHour = now.get(Calendar.HOUR_OF_DAY);
        info.startMinute = now.get(Calendar.MINUTE) + 1;
        info.nextAlarm = 0;
        info.exitAtAlarm = false;
        info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
        info.endMinute = info.startMinute;
        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
        ScheduleCalendar cal = new ScheduleCalendar();
        cal.setSchedule(info);

        Condition condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);

        assertEquals(Condition.STATE_FALSE, condition.state);
    }

    @Test
    public void testEvaluateSubscription_noAlarmExit_endSchedule() {
        Calendar now = getNow();

        // Schedule - 1 hour long; ends now
        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
        info.days = new int[] {Calendar.FRIDAY};
        info.startHour = now.get(Calendar.HOUR_OF_DAY) - 1;
        info.startMinute = now.get(Calendar.MINUTE);
        info.nextAlarm = 0;
        info.exitAtAlarm = false;
        info.endHour = now.get(Calendar.HOUR_OF_DAY);
        info.endMinute = now.get(Calendar.MINUTE);
        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
        ScheduleCalendar cal = new ScheduleCalendar();
        cal.setSchedule(info);

        Condition condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);

        assertEquals(Condition.STATE_FALSE, condition.state);
    }

    @Test
    public void testEvaluateSubscription_alarmSetBeforeInSchedule() {
        Calendar now = getNow();

        // Schedule - 1 hour long; starts now, ends with alarm
        ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
        ScheduleCalendar cal = new ScheduleCalendar();
        cal.setSchedule(info);

        // an hour before start, update with an alarm that will fire during the schedule
        mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis() - 1000, now.getTimeInMillis() + 1000);

        // at start, should be in dnd
        Condition condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
        assertEquals(Condition.STATE_TRUE, condition.state);

        // at alarm fire time, should exit dnd
        assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
        assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
                cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
        condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis() + 1000, 0);
        assertEquals(Condition.STATE_FALSE, condition.state);
    }

    @Test
    public void testEvaluateSubscription_alarmSetInSchedule() {
        Calendar now = getNow();

        // Schedule - 1 hour long; starts now, ends with alarm
        ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
        ScheduleCalendar cal = new ScheduleCalendar();
        cal.setSchedule(info);

        // at start, should be in dnd
        Condition condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis(), 0);
        assertEquals(Condition.STATE_TRUE, condition.state);

        // in schedule, update with alarm time, should be in dnd
        condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis() + 500, now.getTimeInMillis() + 1000);
        assertEquals(Condition.STATE_TRUE, condition.state);

        // at alarm fire time, should exit dnd
        assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
        assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
                cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
        condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis() + 1000, 0);
        assertEquals(Condition.STATE_FALSE, condition.state);
    }

    @Test
    public void testEvaluateSubscription_earlierAlarmSet() {
        Calendar now = getNow();

        // Schedule - 1 hour long; starts now, ends with alarm
        ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
        ScheduleCalendar cal = new ScheduleCalendar();
        cal.setSchedule(info);

        // at start, should be in dnd, alarm in 2000 ms
        Condition condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 2000);
        assertEquals(Condition.STATE_TRUE, condition.state);

        // in schedule, update with earlier alarm time, should be in dnd
        condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis() + 500, now.getTimeInMillis() + 1000);
        assertEquals(Condition.STATE_TRUE, condition.state);

        // at earliest alarm fire time, should exit dnd
        assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
        assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
                cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
        condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis() + 1000, 0);
        assertEquals(Condition.STATE_FALSE, condition.state);
    }

    @Test
    public void testEvaluateSubscription_laterAlarmSet() {
        Calendar now = getNow();

        // Schedule - 1 hour long; starts now, ends with alarm
        ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
        ScheduleCalendar cal = new ScheduleCalendar();
        cal.setSchedule(info);

        // at start, should be in dnd, alarm in 500 ms
        Condition condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 500);
        assertEquals(Condition.STATE_TRUE, condition.state);

        // in schedule, update with nextAlarm = later alarm time (1000), should be in dnd
        condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis() + 250, now.getTimeInMillis() + 1000);
        assertEquals(Condition.STATE_TRUE, condition.state);

        // at next alarm fire time (1000), should exit dnd
        assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
        assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
                cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
        condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis() + 1000, 0);
        assertEquals(Condition.STATE_FALSE, condition.state);
    }

    @Test
    public void testEvaluateSubscription_alarmCanceled() {
        Calendar now = getNow();

        // Schedule - 1 hour long; starts now, ends with alarm
        ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
        ScheduleCalendar cal = new ScheduleCalendar();
        cal.setSchedule(info);

        // at start, should be in dnd, alarm in 500 ms
        Condition condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 500);
        assertEquals(Condition.STATE_TRUE, condition.state);

        // in schedule, cancel alarm
        condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis() + 250, 0);
        assertEquals(Condition.STATE_TRUE, condition.state);

        // at previous alarm time, should not exit DND
        assertTrue(cal.isInSchedule(now.getTimeInMillis() + 500));
        assertFalse(cal.shouldExitForAlarm(now.getTimeInMillis() + 500));
        condition = mService.evaluateSubscriptionLocked(
                conditionId, cal, now.getTimeInMillis() + 500, 0);
        assertEquals(Condition.STATE_TRUE, condition.state);

        // end of schedule, exit DND
        now.add(Calendar.HOUR_OF_DAY, 1);
        condition = mService.evaluateSubscriptionLocked(conditionId, cal, now.getTimeInMillis(), 0);
        assertEquals(Condition.STATE_FALSE, condition.state);
    }

    @Test
    public void testGetPendingIntent() {
        PendingIntent pi = mService.getPendingIntent(1000);
        assertEquals(PackageManagerService.PLATFORM_PACKAGE_NAME, pi.getIntent().getPackage());
    }

    private Calendar getNow() {
        Calendar now = new GregorianCalendar();
        now.set(Calendar.HOUR_OF_DAY, 14);
        now.set(Calendar.MINUTE, 16);
        now.set(Calendar.SECOND, 0);
        now.set(Calendar.MILLISECOND, 0);
        now.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
        return now;
    }

    private ZenModeConfig.ScheduleInfo getScheduleEndsInHour(Calendar now) {
        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
        info.days = new int[] {Calendar.FRIDAY};
        info.startHour = now.get(Calendar.HOUR_OF_DAY);
        info.startMinute = now.get(Calendar.MINUTE);
        info.exitAtAlarm = true;
        info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
        info.endMinute = now.get(Calendar.MINUTE);
        return info;
    }
}
