/*
 * 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 com.android.networkstack.tethering;

import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
import static android.net.NetworkStats.METERED_NO;
import static android.net.NetworkStats.ROAMING_NO;
import static android.net.NetworkStats.SET_DEFAULT;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.NetworkStats.UID_TETHERING;
import static android.net.RouteInfo.RTN_UNICAST;
import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;

import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_IFACE;
import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_UID;
import static com.android.networkstack.tethering.OffloadHardwareInterface.ForwardedStats;
import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0;
import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_1;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
import static com.android.testutils.MiscAsserts.assertContainsAll;
import static com.android.testutils.MiscAsserts.assertThrows;
import static com.android.testutils.NetworkStatsUtilsKt.assertNetworkStatsEquals;

import static junit.framework.Assert.assertNotNull;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.annotation.NonNull;
import android.app.usage.NetworkStatsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.NetworkStats;
import android.net.NetworkStats.Entry;
import android.net.RouteInfo;
import android.net.netstats.provider.NetworkStatsProvider;
import android.net.util.SharedLog;
import android.os.Build;
import android.os.Handler;
import android.os.test.TestLooper;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.test.mock.MockContentResolver;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.internal.util.test.FakeSettingsProvider;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.TestableNetworkStatsProviderCbBinder;

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

import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class OffloadControllerTest {
    @Rule
    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();

    private static final String RNDIS0 = "test_rndis0";
    private static final String RMNET0 = "test_rmnet_data0";
    private static final String WLAN0 = "test_wlan0";

    private static final String IPV6_LINKLOCAL = "fe80::/64";
    private static final String IPV6_DOC_PREFIX = "2001:db8::/64";
    private static final String IPV6_DISCARD_PREFIX = "100::/64";
    private static final String USB_PREFIX = "192.168.42.0/24";
    private static final String WIFI_PREFIX = "192.168.43.0/24";
    private static final long WAIT_FOR_IDLE_TIMEOUT = 2 * 1000;

    @Mock private OffloadHardwareInterface mHardware;
    @Mock private ApplicationInfo mApplicationInfo;
    @Mock private Context mContext;
    @Mock private NetworkStatsManager mStatsManager;
    @Mock private TetheringConfiguration mTetherConfig;
    // Late init since methods must be called by the thread that created this object.
    private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
    private OffloadController.OffloadTetheringStatsProvider mTetherStatsProvider;
    private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
            ArgumentCaptor.forClass(ArrayList.class);
    private final ArgumentCaptor<OffloadHardwareInterface.ControlCallback> mControlCallbackCaptor =
            ArgumentCaptor.forClass(OffloadHardwareInterface.ControlCallback.class);
    private MockContentResolver mContentResolver;
    private final TestLooper mTestLooper = new TestLooper();
    private OffloadController.Dependencies mDeps = new OffloadController.Dependencies() {
        @Override
        public TetheringConfiguration getTetherConfig() {
            return mTetherConfig;
        }
    };

    @Before public void setUp() {
        MockitoAnnotations.initMocks(this);
        when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo);
        when(mContext.getPackageName()).thenReturn("OffloadControllerTest");
        mContentResolver = new MockContentResolver(mContext);
        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
        when(mContext.getContentResolver()).thenReturn(mContentResolver);
        FakeSettingsProvider.clearSettingsProvider();
        when(mTetherConfig.getOffloadPollInterval()).thenReturn(-1); // Disabled.
    }

    @After public void tearDown() throws Exception {
        FakeSettingsProvider.clearSettingsProvider();
    }

    private void setupFunctioningHardwareInterface(int controlVersion) {
        when(mHardware.initOffloadConfig()).thenReturn(true);
        when(mHardware.initOffloadControl(mControlCallbackCaptor.capture()))
                .thenReturn(controlVersion);
        when(mHardware.setUpstreamParameters(anyString(), any(), any(), any())).thenReturn(true);
        when(mHardware.getForwardedStats(any())).thenReturn(new ForwardedStats());
        when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true);
        when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(true);
    }

    private void enableOffload() {
        Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0);
    }

    private void setOffloadPollInterval(int interval) {
        when(mTetherConfig.getOffloadPollInterval()).thenReturn(interval);
    }

    private void waitForIdle() {
        mTestLooper.dispatchAll();
    }

    private OffloadController makeOffloadController() throws Exception {
        OffloadController offload = new OffloadController(new Handler(mTestLooper.getLooper()),
                mHardware, mContentResolver, mStatsManager, new SharedLog("test"), mDeps);
        final ArgumentCaptor<OffloadController.OffloadTetheringStatsProvider>
                tetherStatsProviderCaptor =
                ArgumentCaptor.forClass(OffloadController.OffloadTetheringStatsProvider.class);
        verify(mStatsManager).registerNetworkStatsProvider(anyString(),
                tetherStatsProviderCaptor.capture());
        reset(mStatsManager);
        mTetherStatsProvider = tetherStatsProviderCaptor.getValue();
        assertNotNull(mTetherStatsProvider);
        mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
        mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
        return offload;
    }

    @Test
    public void testStartStop() throws Exception {
        stopOffloadController(
                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/));
        stopOffloadController(
                startOffloadController(OFFLOAD_HAL_VERSION_1_1, true /*expectStart*/));
    }

    @NonNull
    private OffloadController startOffloadController(int controlVersion, boolean expectStart)
            throws Exception {
        setupFunctioningHardwareInterface(controlVersion);
        final OffloadController offload = makeOffloadController();
        offload.start();

        final InOrder inOrder = inOrder(mHardware);
        inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled();
        inOrder.verify(mHardware, times(expectStart ? 1 : 0)).initOffloadConfig();
        inOrder.verify(mHardware, times(expectStart ? 1 : 0)).initOffloadControl(
                any(OffloadHardwareInterface.ControlCallback.class));
        inOrder.verifyNoMoreInteractions();
        // Clear counters only instead of whole mock to preserve the mocking setup.
        clearInvocations(mHardware);
        return offload;
    }

    private void stopOffloadController(final OffloadController offload) throws Exception {
        final InOrder inOrder = inOrder(mHardware);
        offload.stop();
        inOrder.verify(mHardware, times(1)).stopOffloadControl();
        inOrder.verifyNoMoreInteractions();
        reset(mHardware);
    }

    @Test
    public void testNoSettingsValueDefaultDisabledDoesNotStart() throws Exception {
        when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(1);
        assertThrows(SettingNotFoundException.class, () ->
                Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED));
        startOffloadController(OFFLOAD_HAL_VERSION_1_0, false /*expectStart*/);
    }

    @Test
    public void testNoSettingsValueDefaultEnabledDoesStart() throws Exception {
        when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(0);
        assertThrows(SettingNotFoundException.class, () ->
                Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED));
        startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
    }

    @Test
    public void testSettingsAllowsStart() throws Exception {
        Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0);
        startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
    }

    @Test
    public void testSettingsDisablesStart() throws Exception {
        Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 1);
        startOffloadController(OFFLOAD_HAL_VERSION_1_0, false /*expectStart*/);
    }

    @Test
    public void testSetUpstreamLinkPropertiesWorking() throws Exception {
        enableOffload();
        final OffloadController offload =
                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);

        // In reality, the UpstreamNetworkMonitor would have passed down to us
        // a covering set of local prefixes representing a minimum essential
        // set plus all the prefixes on networks with network agents.
        //
        // We simulate that there, and then add upstream elements one by one
        // and watch what happens.
        final Set<IpPrefix> minimumLocalPrefixes = new HashSet<>();
        for (String s : new String[]{
                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"}) {
            minimumLocalPrefixes.add(new IpPrefix(s));
        }
        offload.setLocalPrefixes(minimumLocalPrefixes);
        final InOrder inOrder = inOrder(mHardware);
        inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
        ArrayList<String> localPrefixes = mStringArrayCaptor.getValue();
        assertEquals(4, localPrefixes.size());
        assertContainsAll(localPrefixes,
                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64");
        inOrder.verifyNoMoreInteractions();

        offload.setUpstreamLinkProperties(null);
        // No change in local addresses means no call to setLocalPrefixes().
        inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
        // This LinkProperties value does not differ from the default upstream.
        // There should be no extraneous call to setUpstreamParameters().
        inOrder.verify(mHardware, never()).setUpstreamParameters(
                anyObject(), anyObject(), anyObject(), anyObject());
        inOrder.verifyNoMoreInteractions();

        final LinkProperties lp = new LinkProperties();

        final String testIfName = "rmnet_data17";
        lp.setInterfaceName(testIfName);
        offload.setUpstreamLinkProperties(lp);
        // No change in local addresses means no call to setLocalPrefixes().
        inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
                eq(testIfName), eq(null), eq(null), eq(null));
        inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
        inOrder.verifyNoMoreInteractions();

        final String ipv4Addr = "192.0.2.5";
        final String linkAddr = ipv4Addr + "/24";
        lp.addLinkAddress(new LinkAddress(linkAddr));
        lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, null, RTN_UNICAST));
        offload.setUpstreamLinkProperties(lp);
        // IPv4 prefixes and addresses on the upstream are simply left as whole
        // prefixes (already passed in from UpstreamNetworkMonitor code). If a
        // tethering client sends traffic to the IPv4 default router or other
        // clients on the upstream this will not be hardware-forwarded, and that
        // should be fine for now. Ergo: no change in local addresses, no call
        // to setLocalPrefixes().
        inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
                eq(testIfName), eq(ipv4Addr), eq(null), eq(null));
        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
        inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
        inOrder.verifyNoMoreInteractions();

        final String ipv4Gateway = "192.0.2.1";
        lp.addRoute(new RouteInfo(null, InetAddress.getByName(ipv4Gateway), null, RTN_UNICAST));
        offload.setUpstreamLinkProperties(lp);
        // No change in local addresses means no call to setLocalPrefixes().
        inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
                eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), eq(null));
        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
        inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
        inOrder.verifyNoMoreInteractions();

        final String ipv6Gw1 = "fe80::cafe";
        lp.addRoute(new RouteInfo(null, InetAddress.getByName(ipv6Gw1), null, RTN_UNICAST));
        offload.setUpstreamLinkProperties(lp);
        // No change in local addresses means no call to setLocalPrefixes().
        inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
                eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
        ArrayList<String> v6gws = mStringArrayCaptor.getValue();
        assertEquals(1, v6gws.size());
        assertTrue(v6gws.contains(ipv6Gw1));
        inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
        inOrder.verifyNoMoreInteractions();

        final String ipv6Gw2 = "fe80::d00d";
        lp.addRoute(new RouteInfo(null, InetAddress.getByName(ipv6Gw2), null, RTN_UNICAST));
        offload.setUpstreamLinkProperties(lp);
        // No change in local addresses means no call to setLocalPrefixes().
        inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
                eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
        v6gws = mStringArrayCaptor.getValue();
        assertEquals(2, v6gws.size());
        assertTrue(v6gws.contains(ipv6Gw1));
        assertTrue(v6gws.contains(ipv6Gw2));
        inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
        inOrder.verifyNoMoreInteractions();

        final LinkProperties stacked = new LinkProperties();
        stacked.setInterfaceName("stacked");
        stacked.addLinkAddress(new LinkAddress("192.0.2.129/25"));
        stacked.addRoute(new RouteInfo(null, InetAddress.getByName("192.0.2.254"), null,
                RTN_UNICAST));
        stacked.addRoute(new RouteInfo(null, InetAddress.getByName("fe80::bad:f00"), null,
                RTN_UNICAST));
        assertTrue(lp.addStackedLink(stacked));
        offload.setUpstreamLinkProperties(lp);
        // No change in local addresses means no call to setLocalPrefixes().
        inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
                eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
        v6gws = mStringArrayCaptor.getValue();
        assertEquals(2, v6gws.size());
        assertTrue(v6gws.contains(ipv6Gw1));
        assertTrue(v6gws.contains(ipv6Gw2));
        inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
        inOrder.verifyNoMoreInteractions();

        // Add in some IPv6 upstream info. When there is a tethered downstream
        // making use of the IPv6 prefix we would expect to see the /64 route
        // removed from "local prefixes" and /128s added for the upstream IPv6
        // addresses.  This is not yet implemented, and for now we simply
        // expect to see these /128s.
        lp.addRoute(new RouteInfo(new IpPrefix("2001:db8::/64"), null, null, RTN_UNICAST));
        // "2001:db8::/64" plus "assigned" ASCII in hex
        lp.addLinkAddress(new LinkAddress("2001:db8::6173:7369:676e:6564/64"));
        // "2001:db8::/64" plus "random" ASCII in hex
        lp.addLinkAddress(new LinkAddress("2001:db8::7261:6e64:6f6d/64"));
        offload.setUpstreamLinkProperties(lp);
        inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
        localPrefixes = mStringArrayCaptor.getValue();
        assertEquals(6, localPrefixes.size());
        assertContainsAll(localPrefixes,
                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64",
                "2001:db8::6173:7369:676e:6564/128", "2001:db8::7261:6e64:6f6d/128");
        // The relevant parts of the LinkProperties have not changed, but at the
        // moment we do not de-dup upstream LinkProperties this carefully.
        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
                eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
        v6gws = mStringArrayCaptor.getValue();
        assertEquals(2, v6gws.size());
        assertTrue(v6gws.contains(ipv6Gw1));
        assertTrue(v6gws.contains(ipv6Gw2));
        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
        inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
        inOrder.verifyNoMoreInteractions();

        // Completely identical LinkProperties updates are de-duped.
        offload.setUpstreamLinkProperties(lp);
        // This LinkProperties value does not differ from the default upstream.
        // There should be no extraneous call to setUpstreamParameters().
        inOrder.verify(mHardware, never()).setUpstreamParameters(
                anyObject(), anyObject(), anyObject(), anyObject());
        inOrder.verifyNoMoreInteractions();
    }

    private static @NonNull Entry buildTestEntry(@NonNull OffloadController.StatsType how,
            @NonNull String iface, long rxBytes, long txBytes) {
        return new Entry(iface, how == STATS_PER_IFACE ? UID_ALL : UID_TETHERING, SET_DEFAULT,
                TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, 0L,
                txBytes, 0L, 0L);
    }

    @Test
    public void testGetForwardedStats() throws Exception {
        enableOffload();
        final OffloadController offload =
                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);

        final String ethernetIface = "eth1";
        final String mobileIface = "rmnet_data0";

        when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(
                new ForwardedStats(12345, 54321));
        when(mHardware.getForwardedStats(eq(mobileIface))).thenReturn(
                new ForwardedStats(999, 99999));

        final InOrder inOrder = inOrder(mHardware);

        final LinkProperties lp = new LinkProperties();
        lp.setInterfaceName(ethernetIface);
        offload.setUpstreamLinkProperties(lp);
        // Previous upstream was null, so no stats are fetched.
        inOrder.verify(mHardware, never()).getForwardedStats(any());

        lp.setInterfaceName(mobileIface);
        offload.setUpstreamLinkProperties(lp);
        // Expect that we fetch stats from the previous upstream.
        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(ethernetIface));

        lp.setInterfaceName(ethernetIface);
        offload.setUpstreamLinkProperties(lp);
        // Expect that we fetch stats from the previous upstream.
        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(mobileIface));

        // Verify that the fetched stats are stored.
        final NetworkStats ifaceStats = mTetherStatsProvider.getTetherStats(STATS_PER_IFACE);
        final NetworkStats uidStats = mTetherStatsProvider.getTetherStats(STATS_PER_UID);
        final NetworkStats expectedIfaceStats = new NetworkStats(0L, 2)
                .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 999, 99999))
                .addEntry(buildTestEntry(STATS_PER_IFACE, ethernetIface, 12345, 54321));

        final NetworkStats expectedUidStats = new NetworkStats(0L, 2)
                .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 999, 99999))
                .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 12345, 54321));

        assertNetworkStatsEquals(expectedIfaceStats, ifaceStats);
        assertNetworkStatsEquals(expectedUidStats, uidStats);

        // Force pushing stats update to verify the stats reported.
        mTetherStatsProvider.pushTetherStats();
        mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats);

        when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(
                new ForwardedStats(100000, 100000));
        offload.setUpstreamLinkProperties(null);
        // Expect that we first clear the HAL's upstream parameters.
        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
                eq(""), eq("0.0.0.0"), eq("0.0.0.0"), eq(null));
        // Expect that we fetch stats from the previous upstream.
        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(ethernetIface));

        // There is no current upstream, so no stats are fetched.
        inOrder.verify(mHardware, never()).getForwardedStats(any());
        inOrder.verifyNoMoreInteractions();

        // Verify that the stored stats is accumulated.
        final NetworkStats ifaceStatsAccu = mTetherStatsProvider.getTetherStats(STATS_PER_IFACE);
        final NetworkStats uidStatsAccu = mTetherStatsProvider.getTetherStats(STATS_PER_UID);
        final NetworkStats expectedIfaceStatsAccu = new NetworkStats(0L, 2)
                .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 999, 99999))
                .addEntry(buildTestEntry(STATS_PER_IFACE, ethernetIface, 112345, 154321));

        final NetworkStats expectedUidStatsAccu = new NetworkStats(0L, 2)
                .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 999, 99999))
                .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 112345, 154321));

        assertNetworkStatsEquals(expectedIfaceStatsAccu, ifaceStatsAccu);
        assertNetworkStatsEquals(expectedUidStatsAccu, uidStatsAccu);

        // Verify that only diff of stats is reported.
        mTetherStatsProvider.pushTetherStats();
        final NetworkStats expectedIfaceStatsDiff = new NetworkStats(0L, 2)
                .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 0, 0))
                .addEntry(buildTestEntry(STATS_PER_IFACE, ethernetIface, 100000, 100000));

        final NetworkStats expectedUidStatsDiff = new NetworkStats(0L, 2)
                .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 0, 0))
                .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 100000, 100000));
        mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStatsDiff,
                expectedUidStatsDiff);
    }

    /**
     * Test OffloadController with different combinations of HAL and framework versions can set
     * data warning and/or limit correctly.
     */
    @Test
    public void testSetDataWarningAndLimit() throws Exception {
        // Verify the OffloadController is called by R framework, where the framework doesn't send
        // warning.
        // R only uses HAL 1.0.
        checkSetDataWarningAndLimit(false, OFFLOAD_HAL_VERSION_1_0);
        // Verify the OffloadController is called by S+ framework, where the framework sends
        // warning along with limit.
        checkSetDataWarningAndLimit(true, OFFLOAD_HAL_VERSION_1_0);
        checkSetDataWarningAndLimit(true, OFFLOAD_HAL_VERSION_1_1);
    }

    private void checkSetDataWarningAndLimit(boolean isProviderSetWarning, int controlVersion)
            throws Exception {
        enableOffload();
        final OffloadController offload =
                startOffloadController(controlVersion, true /*expectStart*/);

        final String ethernetIface = "eth1";
        final String mobileIface = "rmnet_data0";
        final long ethernetLimit = 12345;
        final long mobileWarning = 123456;
        final long mobileLimit = 12345678;

        final LinkProperties lp = new LinkProperties();
        lp.setInterfaceName(ethernetIface);

        final InOrder inOrder = inOrder(mHardware);
        when(mHardware.setUpstreamParameters(
                any(), any(), any(), any())).thenReturn(true);
        when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true);
        when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(true);
        offload.setUpstreamLinkProperties(lp);
        // Applying an interface sends the initial quota to the hardware.
        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
            inOrder.verify(mHardware).setDataWarningAndLimit(ethernetIface, Long.MAX_VALUE,
                    Long.MAX_VALUE);
        } else {
            inOrder.verify(mHardware).setDataLimit(ethernetIface, Long.MAX_VALUE);
        }
        inOrder.verifyNoMoreInteractions();

        // Verify that set to unlimited again won't cause duplicated calls to the hardware.
        if (isProviderSetWarning) {
            mTetherStatsProvider.onSetWarningAndLimit(ethernetIface,
                    NetworkStatsProvider.QUOTA_UNLIMITED, NetworkStatsProvider.QUOTA_UNLIMITED);
        } else {
            mTetherStatsProvider.onSetLimit(ethernetIface, NetworkStatsProvider.QUOTA_UNLIMITED);
        }
        waitForIdle();
        inOrder.verifyNoMoreInteractions();

        // Applying an interface quota to the current upstream immediately sends it to the hardware.
        if (isProviderSetWarning) {
            mTetherStatsProvider.onSetWarningAndLimit(ethernetIface,
                    NetworkStatsProvider.QUOTA_UNLIMITED, ethernetLimit);
        } else {
            mTetherStatsProvider.onSetLimit(ethernetIface, ethernetLimit);
        }
        waitForIdle();
        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
            inOrder.verify(mHardware).setDataWarningAndLimit(ethernetIface, Long.MAX_VALUE,
                    ethernetLimit);
        } else {
            inOrder.verify(mHardware).setDataLimit(ethernetIface, ethernetLimit);
        }
        inOrder.verifyNoMoreInteractions();

        // Applying an interface quota to another upstream does not take any immediate action.
        if (isProviderSetWarning) {
            mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit);
        } else {
            mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
        }
        waitForIdle();
        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
            inOrder.verify(mHardware, never()).setDataWarningAndLimit(anyString(), anyLong(),
                    anyLong());
        } else {
            inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong());
        }

        // Switching to that upstream causes the quota to be applied if the parameters were applied
        // correctly.
        lp.setInterfaceName(mobileIface);
        offload.setUpstreamLinkProperties(lp);
        waitForIdle();
        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
            inOrder.verify(mHardware).setDataWarningAndLimit(mobileIface,
                    isProviderSetWarning ? mobileWarning : Long.MAX_VALUE,
                    mobileLimit);
        } else {
            inOrder.verify(mHardware).setDataLimit(mobileIface, mobileLimit);
        }

        // Setting a limit of NetworkStatsProvider.QUOTA_UNLIMITED causes the limit to be set
        // to Long.MAX_VALUE.
        if (isProviderSetWarning) {
            mTetherStatsProvider.onSetWarningAndLimit(mobileIface,
                    NetworkStatsProvider.QUOTA_UNLIMITED, NetworkStatsProvider.QUOTA_UNLIMITED);
        } else {
            mTetherStatsProvider.onSetLimit(mobileIface, NetworkStatsProvider.QUOTA_UNLIMITED);
        }
        waitForIdle();
        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
            inOrder.verify(mHardware).setDataWarningAndLimit(mobileIface, Long.MAX_VALUE,
                    Long.MAX_VALUE);
        } else {
            inOrder.verify(mHardware).setDataLimit(mobileIface, Long.MAX_VALUE);
        }

        // If setting upstream parameters fails, then the data warning and limit is not set.
        when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(false);
        lp.setInterfaceName(ethernetIface);
        offload.setUpstreamLinkProperties(lp);
        if (isProviderSetWarning) {
            mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit);
        } else {
            mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
        }
        waitForIdle();
        inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong());
        inOrder.verify(mHardware, never()).setDataWarningAndLimit(anyString(), anyLong(),
                anyLong());

        // If setting the data warning and/or limit fails while changing upstreams, offload is
        // stopped.
        when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(true);
        when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(false);
        when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(false);
        lp.setInterfaceName(mobileIface);
        offload.setUpstreamLinkProperties(lp);
        if (isProviderSetWarning) {
            mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit);
        } else {
            mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
        }
        waitForIdle();
        inOrder.verify(mHardware).getForwardedStats(ethernetIface);
        inOrder.verify(mHardware).stopOffloadControl();
    }

    @Test
    public void testDataWarningAndLimitCallback_LimitReached() throws Exception {
        enableOffload();
        startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);

        final OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
        callback.onStoppedLimitReached();
        mTetherStatsProviderCb.expectNotifyStatsUpdated();

        if (isAtLeastS()) {
            mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
        } else {
            mTetherStatsProviderCb.expectLegacyNotifyLimitReached();
        }
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.R)  // HAL 1.1 is only supported from S
    public void testDataWarningAndLimitCallback_WarningReached() throws Exception {
        startOffloadController(OFFLOAD_HAL_VERSION_1_1, true /*expectStart*/);
        final OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
        callback.onWarningReached();
        mTetherStatsProviderCb.expectNotifyStatsUpdated();
        mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
    }

    @Test
    public void testAddRemoveDownstreams() throws Exception {
        enableOffload();
        final OffloadController offload =
                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
        final InOrder inOrder = inOrder(mHardware);

        // Tethering makes several calls to setLocalPrefixes() before add/remove
        // downstream calls are made. This is not tested here; only the behavior
        // of notifyDownstreamLinkProperties() and removeDownstreamInterface()
        // are tested.

        // [1] USB tethering is started.
        final LinkProperties usbLinkProperties = new LinkProperties();
        usbLinkProperties.setInterfaceName(RNDIS0);
        usbLinkProperties.addLinkAddress(new LinkAddress("192.168.42.1/24"));
        usbLinkProperties.addRoute(
                new RouteInfo(new IpPrefix(USB_PREFIX), null, null, RTN_UNICAST));
        offload.notifyDownstreamLinkProperties(usbLinkProperties);
        inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, USB_PREFIX);
        inOrder.verifyNoMoreInteractions();

        // [2] Routes for IPv6 link-local prefixes should never be added.
        usbLinkProperties.addRoute(
                new RouteInfo(new IpPrefix(IPV6_LINKLOCAL), null, null, RTN_UNICAST));
        offload.notifyDownstreamLinkProperties(usbLinkProperties);
        inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString());
        inOrder.verifyNoMoreInteractions();

        // [3] Add an IPv6 prefix for good measure. Only new offload-able
        // prefixes should be passed to the HAL.
        usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
        usbLinkProperties.addRoute(
                new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX), null, null, RTN_UNICAST));
        offload.notifyDownstreamLinkProperties(usbLinkProperties);
        inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX);
        inOrder.verifyNoMoreInteractions();

        // [4] Adding addresses doesn't affect notifyDownstreamLinkProperties().
        // The address is passed in by a separate setLocalPrefixes() invocation.
        usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::2/64"));
        offload.notifyDownstreamLinkProperties(usbLinkProperties);
        inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString());

        // [5] Differences in local routes are converted into addDownstream()
        // and removeDownstream() invocations accordingly.
        usbLinkProperties.removeRoute(
                new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX), null, RNDIS0, RTN_UNICAST));
        usbLinkProperties.addRoute(
                new RouteInfo(new IpPrefix(IPV6_DISCARD_PREFIX), null, null, RTN_UNICAST));
        offload.notifyDownstreamLinkProperties(usbLinkProperties);
        inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX);
        inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX);
        inOrder.verifyNoMoreInteractions();

        // [6] Removing a downstream interface which was never added causes no
        // interactions with the HAL.
        offload.removeDownstreamInterface(WLAN0);
        inOrder.verifyNoMoreInteractions();

        // [7] Removing an active downstream removes all remaining prefixes.
        offload.removeDownstreamInterface(RNDIS0);
        inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, USB_PREFIX);
        inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX);
        inOrder.verifyNoMoreInteractions();
    }

    @Test
    public void testControlCallbackOnStoppedUnsupportedFetchesAllStats() throws Exception {
        enableOffload();
        final OffloadController offload =
                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);

        // Pretend to set a few different upstreams (only the interface name
        // matters for this test; we're ignoring IP and route information).
        final LinkProperties upstreamLp = new LinkProperties();
        for (String ifname : new String[]{RMNET0, WLAN0, RMNET0}) {
            upstreamLp.setInterfaceName(ifname);
            offload.setUpstreamLinkProperties(upstreamLp);
        }

        // Clear invocation history, especially the getForwardedStats() calls
        // that happen with setUpstreamParameters().
        clearInvocations(mHardware);

        OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
        callback.onStoppedUnsupported();

        // Verify forwarded stats behaviour.
        verify(mHardware, times(1)).getForwardedStats(eq(RMNET0));
        verify(mHardware, times(1)).getForwardedStats(eq(WLAN0));
        // TODO: verify the exact stats reported.
        mTetherStatsProviderCb.expectNotifyStatsUpdated();
        mTetherStatsProviderCb.assertNoCallback();
        verifyNoMoreInteractions(mHardware);
    }

    @Test
    public void testControlCallbackOnSupportAvailableFetchesAllStatsAndPushesAllParameters()
            throws Exception {
        enableOffload();
        final OffloadController offload =
                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);

        // Pretend to set a few different upstreams (only the interface name
        // matters for this test; we're ignoring IP and route information).
        final LinkProperties upstreamLp = new LinkProperties();
        for (String ifname : new String[]{RMNET0, WLAN0, RMNET0}) {
            upstreamLp.setInterfaceName(ifname);
            offload.setUpstreamLinkProperties(upstreamLp);
        }

        // Pretend that some local prefixes and downstreams have been added
        // (and removed, for good measure).
        final Set<IpPrefix> minimumLocalPrefixes = new HashSet<>();
        for (String s : new String[]{
                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"}) {
            minimumLocalPrefixes.add(new IpPrefix(s));
        }
        offload.setLocalPrefixes(minimumLocalPrefixes);

        final LinkProperties usbLinkProperties = new LinkProperties();
        usbLinkProperties.setInterfaceName(RNDIS0);
        usbLinkProperties.addLinkAddress(new LinkAddress("192.168.42.1/24"));
        usbLinkProperties.addRoute(
                new RouteInfo(new IpPrefix(USB_PREFIX), null, null, RTN_UNICAST));
        offload.notifyDownstreamLinkProperties(usbLinkProperties);

        final LinkProperties wifiLinkProperties = new LinkProperties();
        wifiLinkProperties.setInterfaceName(WLAN0);
        wifiLinkProperties.addLinkAddress(new LinkAddress("192.168.43.1/24"));
        wifiLinkProperties.addRoute(
                new RouteInfo(new IpPrefix(WIFI_PREFIX), null, null, RTN_UNICAST));
        wifiLinkProperties.addRoute(
                new RouteInfo(new IpPrefix(IPV6_LINKLOCAL), null, null, RTN_UNICAST));
        // Use a benchmark prefix (RFC 5180 + erratum), since the documentation
        // prefix is included in the excluded prefix list.
        wifiLinkProperties.addLinkAddress(new LinkAddress("2001:2::1/64"));
        wifiLinkProperties.addLinkAddress(new LinkAddress("2001:2::2/64"));
        wifiLinkProperties.addRoute(
                new RouteInfo(new IpPrefix("2001:2::/64"), null, null, RTN_UNICAST));
        offload.notifyDownstreamLinkProperties(wifiLinkProperties);

        offload.removeDownstreamInterface(RNDIS0);

        // Clear invocation history, especially the getForwardedStats() calls
        // that happen with setUpstreamParameters().
        clearInvocations(mHardware);

        OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
        callback.onSupportAvailable();

        // Verify forwarded stats behaviour.
        verify(mHardware, times(1)).getForwardedStats(eq(RMNET0));
        verify(mHardware, times(1)).getForwardedStats(eq(WLAN0));
        mTetherStatsProviderCb.expectNotifyStatsUpdated();
        mTetherStatsProviderCb.assertNoCallback();

        // TODO: verify local prefixes and downstreams are also pushed to the HAL.
        verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
        ArrayList<String> localPrefixes = mStringArrayCaptor.getValue();
        assertEquals(4, localPrefixes.size());
        assertContainsAll(localPrefixes,
                // TODO: The logic to find and exclude downstream IP prefixes
                // is currently in Tethering's OffloadWrapper but must be moved
                // into OffloadController proper. After this, also check for:
                //     "192.168.43.1/32", "2001:2::1/128", "2001:2::2/128"
                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64");
        verify(mHardware, times(1)).addDownstreamPrefix(WLAN0, "192.168.43.0/24");
        verify(mHardware, times(1)).addDownstreamPrefix(WLAN0, "2001:2::/64");
        verify(mHardware, times(1)).setUpstreamParameters(eq(RMNET0), any(), any(), any());
        verify(mHardware, times(1)).setDataLimit(eq(RMNET0), anyLong());
        verifyNoMoreInteractions(mHardware);
    }

    @Test
    public void testOnSetAlert() throws Exception {
        enableOffload();
        setOffloadPollInterval(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
        final OffloadController offload =
                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);

        // Initialize with fake eth upstream.
        final String ethernetIface = "eth1";
        InOrder inOrder = inOrder(mHardware);
        offload.setUpstreamLinkProperties(makeEthernetLinkProperties());
        // Previous upstream was null, so no stats are fetched.
        inOrder.verify(mHardware, never()).getForwardedStats(any());

        // Verify that set quota to 0 will immediately triggers an callback.
        mTetherStatsProvider.onSetAlert(0);
        waitForIdle();
        mTetherStatsProviderCb.expectNotifyAlertReached();

        // Verify that notifyAlertReached never fired if quota is not yet reached.
        when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(
                new ForwardedStats(0, 0));
        mTetherStatsProvider.onSetAlert(100);
        mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
        waitForIdle();
        mTetherStatsProviderCb.assertNoCallback();

        // Verify that notifyAlertReached fired when quota is reached.
        when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(
                new ForwardedStats(50, 50));
        mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
        waitForIdle();
        mTetherStatsProviderCb.expectNotifyAlertReached();

        // Verify that set quota with UNLIMITED won't trigger any callback, and won't fetch
        // any stats since the polling is stopped.
        reset(mHardware);
        mTetherStatsProvider.onSetAlert(NetworkStatsProvider.QUOTA_UNLIMITED);
        mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
        waitForIdle();
        mTetherStatsProviderCb.assertNoCallback();
        verify(mHardware, never()).getForwardedStats(any());
    }

    private static LinkProperties makeEthernetLinkProperties() {
        final String ethernetIface = "eth1";
        final LinkProperties lp = new LinkProperties();
        lp.setInterfaceName(ethernetIface);
        return lp;
    }

    private void checkSoftwarePollingUsed(int controlVersion) throws Exception {
        enableOffload();
        setOffloadPollInterval(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
        OffloadController offload =
                startOffloadController(controlVersion, true /*expectStart*/);
        offload.setUpstreamLinkProperties(makeEthernetLinkProperties());
        mTetherStatsProvider.onSetAlert(0);
        waitForIdle();
        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
            mTetherStatsProviderCb.assertNoCallback();
        } else {
            mTetherStatsProviderCb.expectNotifyAlertReached();
        }
        verify(mHardware, never()).getForwardedStats(any());
    }

    @Test
    public void testSoftwarePollingUsed() throws Exception {
        checkSoftwarePollingUsed(OFFLOAD_HAL_VERSION_1_0);
        checkSoftwarePollingUsed(OFFLOAD_HAL_VERSION_1_1);
    }
}
