/*
 * Copyright (C) 2016 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 com.android.server.wifi.scanner;

import static com.android.server.wifi.ScanTestUtil.NativeScanSettingsBuilder;
import static com.android.server.wifi.ScanTestUtil.setupMockChannels;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import android.app.test.TestAlarmManager;
import android.content.Context;
import android.net.wifi.WifiScanner;
import android.os.SystemClock;
import android.os.test.TestLooper;

import androidx.test.filters.SmallTest;

import com.android.server.wifi.Clock;
import com.android.server.wifi.MockResources;
import com.android.server.wifi.MockWifiMonitor;
import com.android.server.wifi.ScanResults;
import com.android.server.wifi.WifiBaseTest;
import com.android.server.wifi.WifiGlobals;
import com.android.server.wifi.WifiMonitor;
import com.android.server.wifi.WifiNative;
import com.android.server.wifi.scanner.ChannelHelper.ChannelCollection;

import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.List;
import java.util.Set;

/**
 * Unit tests for {@link com.android.server.wifi.scanner.WificondScannerImpl.setPnoList}.
 */
@SmallTest
public class WificondPnoScannerTest extends WifiBaseTest {
    private static final String IFACE_NAME = "a_test_interface_name";

    @Mock Context mContext;
    TestAlarmManager mAlarmManager;
    MockWifiMonitor mWifiMonitor;
    TestLooper mLooper;
    @Mock WifiNative mWifiNative;
    MockResources mResources;
    @Mock Clock mClock;
    WificondScannerImpl mScanner;
    @Mock WifiGlobals mWifiGlobals;

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

        mLooper = new TestLooper();
        mAlarmManager = new TestAlarmManager();
        mWifiMonitor = new MockWifiMonitor();
        mResources = new MockResources();

        setupMockChannels(mWifiNative,
                new int[]{2400, 2450},
                new int[]{5150, 5175},
                new int[]{5600, 5650},
                new int[]{5945, 5985},
                new int[]{58320, 60480});

        when(mContext.getSystemService(Context.ALARM_SERVICE))
                .thenReturn(mAlarmManager.getAlarmManager());
        when(mContext.getResources()).thenReturn(mResources);
        when(mClock.getElapsedSinceBootMillis()).thenReturn(SystemClock.elapsedRealtime());
    }

    /**
     * Verify that the HW disconnected PNO scan triggers a wificond PNO scan and invokes the
     * OnPnoNetworkFound callback when the scan results are received.
     */
    @Test
    public void startHwDisconnectedPnoScan() {
        createScannerWithHwPnoScanSupport();

        WifiNative.PnoEventHandler pnoEventHandler = mock(WifiNative.PnoEventHandler.class);
        WifiNative.PnoSettings pnoSettings = createDummyPnoSettings(false);
        ScanResults scanResults = createDummyScanResults();

        InOrder order = inOrder(pnoEventHandler, mWifiNative);
        // Start PNO scan
        startSuccessfulPnoScan(null, pnoSettings, null, pnoEventHandler);
        expectSuccessfulHwDisconnectedPnoScan(order, pnoSettings, pnoEventHandler, scanResults);
        verifyNoMoreInteractions(pnoEventHandler);
    }

    /**
     * Verify that the HW disconnected PNO scan triggers a wificond PNO scan and invokes the
     * OnPnoScanFailed callback when the scan fails.
     */
    @Test
    public void startHwDisconnectedPnoScanFailure() {
        createScannerWithHwPnoScanSupport();

        WifiNative.PnoEventHandler pnoEventHandler = mock(WifiNative.PnoEventHandler.class);
        WifiNative.PnoSettings pnoSettings = createDummyPnoSettings(false);

        InOrder order = inOrder(pnoEventHandler, mWifiNative);

        // Fail PNO scan
        when(mWifiNative.startPnoScan(eq(IFACE_NAME), any(WifiNative.PnoSettings.class)))
                .thenReturn(false);
        assertTrue(mScanner.setHwPnoList(pnoSettings, pnoEventHandler));
        order.verify(mWifiNative).startPnoScan(any(), any(WifiNative.PnoSettings.class));
        order.verify(pnoEventHandler).onPnoScanFailed();
        verifyNoMoreInteractions(pnoEventHandler);
    }

    /**
     * Verify that the HW PNO scan stop failure still resets the PNO scan state.
     * 1. Start Hw PNO.
     * 2. Stop Hw PNO scan which raises a stop command to WifiNative which is failed.
     * 3. Now restart a new PNO scan to ensure that the failure was cleanly handled.
     */
    @Test
    public void ignoreHwDisconnectedPnoScanStopFailure() {
        createScannerWithHwPnoScanSupport();

        WifiNative.PnoEventHandler pnoEventHandler = mock(WifiNative.PnoEventHandler.class);
        WifiNative.PnoSettings pnoSettings = createDummyPnoSettings(false);

        // Start PNO scan
        startSuccessfulPnoScan(null, pnoSettings, null, pnoEventHandler);

        // Fail the PNO stop.
        when(mWifiNative.stopPnoScan(IFACE_NAME)).thenReturn(false);
        assertTrue(mScanner.resetHwPnoList());
        mLooper.dispatchAll();
        verify(mWifiNative).stopPnoScan(IFACE_NAME);

        // Add a new PNO scan request and ensure it runs successfully.
        startSuccessfulPnoScan(null, pnoSettings, null, pnoEventHandler);
        mLooper.dispatchAll();
        InOrder order = inOrder(pnoEventHandler, mWifiNative);
        ScanResults scanResults = createDummyScanResults();
        expectSuccessfulHwDisconnectedPnoScan(order, pnoSettings, pnoEventHandler, scanResults);
        verifyNoMoreInteractions(pnoEventHandler);
    }

    private void createScannerWithHwPnoScanSupport() {
        when(mWifiGlobals.isBackgroundScanSupported()).thenReturn(true);
        mScanner = new WificondScannerImpl(mContext, IFACE_NAME, mWifiGlobals, mWifiNative,
                mWifiMonitor, new WificondChannelHelper(mWifiNative), mLooper.getLooper(),
                mClock);
    }

    private WifiNative.PnoNetwork createDummyPnoNetwork(String ssid) {
        WifiNative.PnoNetwork pnoNetwork = new WifiNative.PnoNetwork();
        pnoNetwork.ssid = ssid;
        return pnoNetwork;
    }

    private WifiNative.PnoSettings createDummyPnoSettings(boolean isConnected) {
        WifiNative.PnoSettings pnoSettings = new WifiNative.PnoSettings();
        pnoSettings.isConnected = isConnected;
        pnoSettings.networkList = new WifiNative.PnoNetwork[2];
        pnoSettings.networkList[0] = createDummyPnoNetwork("ssid_pno_1");
        pnoSettings.networkList[1] = createDummyPnoNetwork("ssid_pno_2");
        return pnoSettings;
    }

    private WifiNative.ScanSettings createDummyScanSettings(boolean allChannelsScanned) {
        WifiNative.ScanSettings settings = new NativeScanSettingsBuilder()
                .withBasePeriod(10000)
                .withMaxApPerScan(10)
                .addBucketWithBand(
                    10000,
                    WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN,
                    allChannelsScanned
                            ? WifiScanner.WIFI_BAND_24_5_WITH_DFS_6_60_GHZ
                            : WifiScanner.WIFI_BAND_24_GHZ)
                .build();
        return settings;
    }

    private ScanResults createDummyScanResults() {
        return ScanResults.create(0, WifiScanner.WIFI_BAND_UNSPECIFIED,  2400, 2450, 2450, 2400,
                2450, 2450, 2400, 2450, 2450);
    }

    private void startSuccessfulPnoScan(WifiNative.ScanSettings scanSettings,
            WifiNative.PnoSettings pnoSettings, WifiNative.ScanEventHandler scanEventHandler,
            WifiNative.PnoEventHandler pnoEventHandler) {
        // Scans succeed
        when(mWifiNative.scan(eq(IFACE_NAME), anyInt(), any(), any(List.class), anyBoolean(),
                any())).thenReturn(WifiScanner.REASON_SUCCEEDED);
        when(mWifiNative.startPnoScan(eq(IFACE_NAME), any(WifiNative.PnoSettings.class)))
                .thenReturn(true);
        when(mWifiNative.stopPnoScan(IFACE_NAME)).thenReturn(true);

        assertTrue(mScanner.setHwPnoList(pnoSettings, pnoEventHandler));
    }

    private Set<Integer> expectedBandScanFreqs(int band) {
        ChannelCollection collection = mScanner.getChannelHelper().createChannelCollection();
        collection.addBand(band);
        return collection.getScanFreqs();
    }

    /**
     * Verify that the PNO scan was successfully started.
     */
    private void expectHwDisconnectedPnoScanStart(InOrder order,
            WifiNative.PnoSettings pnoSettings) {
        // Verify  HW PNO scan started
        order.verify(mWifiNative).startPnoScan(any(), any(WifiNative.PnoSettings.class));
    }

    /**
     *
     * 1. Verify that the PNO scan was successfully started.
     * 2. Send scan results and ensure that the |onPnoNetworkFound| callback was called.
     */
    private void expectSuccessfulHwDisconnectedPnoScan(InOrder order,
            WifiNative.PnoSettings pnoSettings, WifiNative.PnoEventHandler eventHandler,
            ScanResults scanResults) {
        expectHwDisconnectedPnoScanStart(order, pnoSettings);

        // Setup scan results
        when(mWifiNative.getPnoScanResults(IFACE_NAME))
                .thenReturn(scanResults.getScanDetailArrayList());
        when(mWifiNative.getScanResults(IFACE_NAME))
                .thenReturn(scanResults.getScanDetailArrayList());

        // Notify scan has finished
        mWifiMonitor.sendMessage(IFACE_NAME, WifiMonitor.PNO_SCAN_RESULTS_EVENT);
        assertEquals("dispatch message after results event", 1, mLooper.dispatchAll());

        order.verify(eventHandler).onPnoNetworkFound(scanResults.getRawScanResults());
    }

}
