/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package android.hardware.radio.tests.functional;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioTuner;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.Log;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import static org.junit.Assert.*;
import static org.junit.Assume.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.after;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.testng.Assert.assertThrows;

/**
 * A test for broadcast radio API.
 */
@RunWith(AndroidJUnit4.class)
@MediumTest
public class RadioTunerTest {
    private static final String TAG = "BroadcastRadioTests.RadioTuner";

    public final Context mContext = InstrumentationRegistry.getContext();

    private final int kConfigCallbackTimeoutMs = 10000;
    private final int kCancelTimeoutMs = 1000;
    private final int kTuneCallbackTimeoutMs = 30000;
    private final int kFullScanTimeoutMs = 60000;

    private RadioManager mRadioManager;
    private RadioTuner mRadioTuner;
    private RadioManager.ModuleProperties mModule;
    private final List<RadioManager.ModuleProperties> mModules = new ArrayList<>();
    @Mock private RadioTuner.Callback mCallback;

    RadioManager.AmBandDescriptor mAmBandDescriptor;
    RadioManager.FmBandDescriptor mFmBandDescriptor;

    RadioManager.BandConfig mAmBandConfig;
    RadioManager.BandConfig mFmBandConfig;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        // check if radio is supported and skip the test if it's not
        PackageManager packageManager = mContext.getPackageManager();
        boolean isRadioSupported = packageManager.hasSystemFeature(
                PackageManager.FEATURE_BROADCAST_RADIO);
        assumeTrue(isRadioSupported);

        // Check radio access permission
        int res = mContext.checkCallingOrSelfPermission(Manifest.permission.ACCESS_BROADCAST_RADIO);
        assertEquals("ACCESS_BROADCAST_RADIO permission not granted",
                PackageManager.PERMISSION_GRANTED, res);

        mRadioManager = (RadioManager)mContext.getSystemService(Context.RADIO_SERVICE);
        assertNotNull(mRadioManager);

        int status = mRadioManager.listModules(mModules);
        assertEquals(RadioManager.STATUS_OK, status);
        assertFalse(mModules.isEmpty());
    }

    @After
    public void tearDown() {
        mRadioManager = null;
        mModules.clear();
        if (mRadioTuner != null) {
            mRadioTuner.close();
            mRadioTuner = null;
        }
        resetCallback();
    }

    private void openTuner() {
        openTuner(true);
    }

    private void resetCallback() {
        verify(mCallback, atLeast(0)).onMetadataChanged(any());
        verifyNoMoreInteractions(mCallback);
        Mockito.reset(mCallback);
    }

    private void openTuner(boolean withAudio) {
        assertNull(mRadioTuner);

        // find FM band and build its config
        mModule = mModules.get(0);

        for (RadioManager.BandDescriptor band : mModule.getBands()) {
            Log.d(TAG, "Band: " + band);
            int bandType = band.getType();
            if (bandType == RadioManager.BAND_AM || bandType == RadioManager.BAND_AM_HD) {
                mAmBandDescriptor = (RadioManager.AmBandDescriptor)band;
            }
            if (bandType == RadioManager.BAND_FM || bandType == RadioManager.BAND_FM_HD) {
                mFmBandDescriptor = (RadioManager.FmBandDescriptor)band;
            }
        }
        assertNotNull(mAmBandDescriptor);
        assertNotNull(mFmBandDescriptor);
        mAmBandConfig = new RadioManager.AmBandConfig.Builder(mAmBandDescriptor).build();
        mFmBandConfig = new RadioManager.FmBandConfig.Builder(mFmBandDescriptor).build();

        mRadioTuner = mRadioManager.openTuner(mModule.getId(),
                mFmBandConfig, withAudio, mCallback, null);
        assertNotNull(mRadioTuner);
        verify(mCallback, timeout(kConfigCallbackTimeoutMs)).onConfigurationChanged(any());
        resetCallback();

        boolean isAntennaConnected = mRadioTuner.isAntennaConnected();
        assertTrue(isAntennaConnected);
    }

    @Test
    public void testOpenTuner() {
        openTuner();
    }

    @Test
    public void testReopenTuner() throws Throwable {
        openTuner();
        mRadioTuner.close();
        mRadioTuner = null;
        Thread.sleep(100);  // TODO(b/36122635): force reopen
        openTuner();
    }

    @Test
    public void testDoubleClose() {
        openTuner();
        mRadioTuner.close();
        mRadioTuner.close();
    }

    @Test
    public void testUseAfterClose() {
        openTuner();
        mRadioTuner.close();
        int ret = mRadioTuner.cancel();
        assertEquals(RadioManager.STATUS_INVALID_OPERATION, ret);
    }

    @Test
    public void testSetAndGetConfiguration() {
        openTuner();

        // set
        int ret = mRadioTuner.setConfiguration(mAmBandConfig);
        assertEquals(RadioManager.STATUS_OK, ret);
        verify(mCallback, timeout(kConfigCallbackTimeoutMs)).onConfigurationChanged(any());

        // get
        RadioManager.BandConfig[] config = new RadioManager.BandConfig[1];
        ret = mRadioTuner.getConfiguration(config);
        assertEquals(RadioManager.STATUS_OK, ret);

        assertEquals(mAmBandConfig, config[0]);
    }

    @Test
    public void testSetBadConfiguration() throws Throwable {
        openTuner();

        // set bad config
        Constructor<RadioManager.AmBandConfig> configConstr =
                RadioManager.AmBandConfig.class.getDeclaredConstructor(
                        int.class, int.class, int.class, int.class, int.class, boolean.class);
        configConstr.setAccessible(true);
        RadioManager.AmBandConfig badConfig = configConstr.newInstance(
                0 /*region*/, RadioManager.BAND_AM /*type*/,
                10000 /*lowerLimit*/, 1 /*upperLimit*/, 100 /*spacing*/, false /*stereo*/);
        int ret = mRadioTuner.setConfiguration(badConfig);
        assertEquals(RadioManager.STATUS_BAD_VALUE, ret);
        verify(mCallback, never()).onConfigurationChanged(any());

        // set null config
        ret = mRadioTuner.setConfiguration(null);
        assertEquals(RadioManager.STATUS_BAD_VALUE, ret);
        verify(mCallback, never()).onConfigurationChanged(any());

        // setting good config should recover
        ret = mRadioTuner.setConfiguration(mAmBandConfig);
        assertEquals(RadioManager.STATUS_OK, ret);
        verify(mCallback, timeout(kConfigCallbackTimeoutMs)).onConfigurationChanged(any());
    }

    @Test
    public void testMute() {
        openTuner();

        boolean isMuted = mRadioTuner.getMute();
        assertFalse(isMuted);

        int ret = mRadioTuner.setMute(true);
        assertEquals(RadioManager.STATUS_OK, ret);
        isMuted = mRadioTuner.getMute();
        assertTrue(isMuted);

        ret = mRadioTuner.setMute(false);
        assertEquals(RadioManager.STATUS_OK, ret);
        isMuted = mRadioTuner.getMute();
        assertFalse(isMuted);
    }

    @Test
    public void testMuteNoAudio() {
        openTuner(false);

        int ret = mRadioTuner.setMute(false);
        assertEquals(RadioManager.STATUS_ERROR, ret);

        boolean isMuted = mRadioTuner.getMute();
        assertTrue(isMuted);
    }

    @Test
    public void testStep() {
        openTuner();

        int ret = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, true);
        assertEquals(RadioManager.STATUS_OK, ret);
        verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(any());

        resetCallback();

        ret = mRadioTuner.step(RadioTuner.DIRECTION_UP, false);
        assertEquals(RadioManager.STATUS_OK, ret);
        verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(any());
    }

    @Test
    public void testStepLoop() {
        openTuner();

        for (int i = 0; i < 10; i++) {
            Log.d(TAG, "step loop iteration " + (i + 1));

            int ret = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, true);
            assertEquals(RadioManager.STATUS_OK, ret);
            verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(any());

            resetCallback();
        }
    }

    @Test
    public void testTuneAndGetPI() {
        openTuner();

        int channel = mFmBandConfig.getLowerLimit() + mFmBandConfig.getSpacing();

        // test tune
        int ret = mRadioTuner.tune(channel, 0);
        assertEquals(RadioManager.STATUS_OK, ret);
        ArgumentCaptor<RadioManager.ProgramInfo> infoc =
                ArgumentCaptor.forClass(RadioManager.ProgramInfo.class);
        verify(mCallback, timeout(kTuneCallbackTimeoutMs))
                .onProgramInfoChanged(infoc.capture());
        assertEquals(channel, infoc.getValue().getChannel());

        // test getProgramInformation
        RadioManager.ProgramInfo[] info = new RadioManager.ProgramInfo[1];
        ret = mRadioTuner.getProgramInformation(info);
        assertEquals(RadioManager.STATUS_OK, ret);
        assertNotNull(info[0]);
        assertEquals(channel, info[0].getChannel());
        Log.d(TAG, "PI: " + info[0].toString());
    }

    @Test
    public void testDummyCancel() {
        openTuner();

        int ret = mRadioTuner.cancel();
        assertEquals(RadioManager.STATUS_OK, ret);
    }

    @Test
    public void testLateCancel() {
        openTuner();

        int ret = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, false);
        assertEquals(RadioManager.STATUS_OK, ret);
        verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(any());

        int cancelRet = mRadioTuner.cancel();
        assertEquals(RadioManager.STATUS_OK, cancelRet);
    }

    @Test
    public void testScanAndCancel() {
        openTuner();

        /* There is a possible race condition between scan and cancel commands - the scan may finish
         * before cancel command is issued. Thus we accept both outcomes in this test.
         */
        int scanRet = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, true);
        int cancelRet = mRadioTuner.cancel();

        assertEquals(RadioManager.STATUS_OK, scanRet);
        assertEquals(RadioManager.STATUS_OK, cancelRet);

        verify(mCallback, after(kCancelTimeoutMs).atMost(1)).onError(RadioTuner.ERROR_CANCELLED);
        verify(mCallback, atMost(1)).onProgramInfoChanged(any());
    }

    @Test
    public void testStartBackgroundScan() {
        openTuner();

        boolean ret = mRadioTuner.startBackgroundScan();
        boolean isSupported = mModule.isBackgroundScanningSupported();
        assertEquals(isSupported, ret);
    }

    @Test
    public void testGetProgramList() {
        openTuner();

        try {
            Map<String, String> filter = new HashMap<>();
            filter.put("com.google.dummy", "dummy");
            List<RadioManager.ProgramInfo> list = mRadioTuner.getProgramList(filter);
            assertNotNull(list);
        } catch (IllegalStateException e) {
            // the list may or may not be ready at this point
            Log.i(TAG, "Background list is not ready");
        }
    }

    @Test
    public void testTuneFromProgramList() {
        openTuner();

        List<RadioManager.ProgramInfo> list;

        try {
            list = mRadioTuner.getProgramList(null);
            assertNotNull(list);
        } catch (IllegalStateException e) {
            Log.i(TAG, "Background list is not ready, trying to fix it");

            boolean success = mRadioTuner.startBackgroundScan();
            assertTrue(success);
            verify(mCallback, timeout(kFullScanTimeoutMs)).onBackgroundScanComplete();

            list = mRadioTuner.getProgramList(null);
            assertNotNull(list);
        }

        if (list.isEmpty()) {
            Log.i(TAG, "Program list is empty, can't test tune");
            return;
        }

        ProgramSelector sel = list.get(0).getSelector();
        mRadioTuner.tune(sel);
        ArgumentCaptor<RadioManager.ProgramInfo> infoc =
                ArgumentCaptor.forClass(RadioManager.ProgramInfo.class);
        verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(infoc.capture());
        assertEquals(sel, infoc.getValue().getSelector());
    }

    @Test
    public void testForcedAnalog() {
        openTuner();

        boolean isSupported = true;
        boolean isForced;
        try {
            isForced = mRadioTuner.isAnalogForced();
            assertFalse(isForced);
        } catch (IllegalStateException ex) {
            Log.i(TAG, "Forced analog switch is not supported by this tuner");
            isSupported = false;
        }

        if (isSupported) {
            mRadioTuner.setAnalogForced(true);
            isForced = mRadioTuner.isAnalogForced();
            assertTrue(isForced);

            mRadioTuner.setAnalogForced(false);
            isForced = mRadioTuner.isAnalogForced();
            assertFalse(isForced);
        } else {
            assertThrows(IllegalStateException.class, () -> mRadioTuner.setAnalogForced(true));
        }
    }
}
