/*
 * Copyright (C) 2018 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.net.dhcp;

import static android.net.InetAddresses.parseNumericAddress;
import static android.net.dhcp.DhcpLease.HOSTNAME_NONE;
import static android.net.dhcp.DhcpLeaseRepository.CLIENTID_UNSPEC;
import static android.net.dhcp.DhcpLeaseRepository.INETADDR_UNSPEC;

import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.atLeastOnce;
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 static java.lang.String.format;
import static java.util.stream.Collectors.toSet;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.net.IpPrefix;
import android.net.MacAddress;
import android.net.dhcp.DhcpServer.Clock;
import android.os.Binder;
import android.os.RemoteException;

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

import com.android.net.module.util.SharedLog;

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

import java.net.Inet4Address;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class DhcpLeaseRepositoryTest {
    private static final Inet4Address TEST_DEF_ROUTER = parseAddr4("192.168.42.247");
    private static final Inet4Address TEST_SERVER_ADDR = parseAddr4("192.168.42.241");
    private static final Inet4Address TEST_CLIENT_ADDR = parseAddr4("192.168.42.2");
    private static final Inet4Address TEST_RESERVED_ADDR = parseAddr4("192.168.42.243");
    private static final MacAddress TEST_MAC_1 = MacAddress.fromBytes(
            new byte[] { 5, 4, 3, 2, 1, 0 });
    private static final MacAddress TEST_MAC_2 = MacAddress.fromBytes(
            new byte[] { 0, 1, 2, 3, 4, 5 });
    private static final MacAddress TEST_MAC_3 = MacAddress.fromBytes(
            new byte[] { 0, 1, 2, 3, 4, 6 });
    private static final Inet4Address TEST_INETADDR_1 = parseAddr4("192.168.42.248");
    private static final Inet4Address TEST_INETADDR_2 = parseAddr4("192.168.42.249");
    private static final String TEST_HOSTNAME_1 = "hostname1";
    private static final String TEST_HOSTNAME_2 = "hostname2";
    @SuppressLint("NewApi")
    private static final IpPrefix TEST_IP_PREFIX = new IpPrefix(TEST_SERVER_ADDR, 22);
    private static final long TEST_TIME = 100L;
    private static final int TEST_LEASE_TIME_MS = 3_600_000;
    private static final Set<Inet4Address> TEST_EXCL_SET =
            Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
                TEST_SERVER_ADDR, TEST_DEF_ROUTER, TEST_RESERVED_ADDR)));
    private static final int DEFAULT_TARGET_PREFIX_LENGTH = 0;

    @NonNull
    private SharedLog mLog;
    @NonNull
    @Mock
    private Clock mClock;
    @NonNull
    private DhcpLeaseRepository mRepo;
    @NonNull
    @Mock
    private IDhcpEventCallbacks mCallbacks;
    private final Binder mCallbacksBinder = new Binder();

    private static Inet4Address parseAddr4(String inet4Addr) {
        return (Inet4Address) parseNumericAddress(inet4Addr);
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        initDhcpLeaseRepositoryWithOption(null);
    }

    private void initDhcpLeaseRepositoryWithOption(final Inet4Address clientAddr) {
        reset(mCallbacks, mClock);
        mLog = new SharedLog("DhcpLeaseRepositoryTest");
        when(mClock.elapsedRealtime()).thenReturn(TEST_TIME);
        // Use a non-null Binder for linkToDeath
        when(mCallbacks.asBinder()).thenReturn(mCallbacksBinder);
        mRepo = new DhcpLeaseRepository(
                TEST_IP_PREFIX, TEST_EXCL_SET, TEST_LEASE_TIME_MS, clientAddr,
                DEFAULT_TARGET_PREFIX_LENGTH,
                mLog, mClock);
        mRepo.addLeaseCallbacks(mCallbacks);
        verify(mCallbacks, atLeastOnce()).asBinder();
    }

    /**
     * Request a number of addresses through offer/request. Useful to test address exhaustion.
     * @param nAddr Number of addresses to request.
     */
    private Set<Inet4Address> requestAddresses(byte nAddr) throws Exception {
        final HashSet<Inet4Address> addrs = new HashSet<>();
        byte[] hwAddrBytes = new byte[] { 8, 4, 3, 2, 1, 0 };
        for (byte i = 0; i < nAddr; i++) {
            hwAddrBytes[5] = i;
            MacAddress newMac = MacAddress.fromBytes(hwAddrBytes);
            final String hostname = "host_" + i;
            final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, newMac,
                    IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, hostname);

            assertNotNull(lease);
            assertEquals(newMac, lease.getHwAddr());
            assertEquals(hostname, lease.getHostname());
            assertTrue(format("Duplicate address allocated: %s in %s", lease.getNetAddr(), addrs),
                    addrs.add(lease.getNetAddr()));

            requestLeaseSelecting(newMac, lease.getNetAddr(), hostname);
        }

        return addrs;
    }

    @SuppressLint("NewApi")
    @Test
    public void testAddressExhaustion() throws Exception {
        // Use a /28 to quickly run out of addresses
        mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS,
                null /* clientAddr */, DEFAULT_TARGET_PREFIX_LENGTH);

        // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses
        requestAddresses((byte) 11);
        verify(mCallbacks, times(11)).onLeasesChanged(any());

        try {
            mRepo.getOffer(null, TEST_MAC_2,
                    IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
            fail("Should be out of addresses");
        } catch (DhcpLeaseRepository.OutOfAddressesException e) {
            // Expected
        }
        verifyNoMoreInteractions(mCallbacks);
    }

    @SuppressLint("NewApi")
    @Test
    public void testUpdateParams_LeaseCleanup() throws Exception {
        // Inside /28:
        final Inet4Address reqAddrIn28 = parseAddr4("192.168.42.242");
        final Inet4Address declinedAddrIn28 = parseAddr4("192.168.42.245");

        // Inside /28, but not available there (first address of the range)
        final Inet4Address declinedFirstAddrIn28 = parseAddr4("192.168.42.240");

        final DhcpLease reqAddrIn28Lease = requestLeaseSelecting(TEST_MAC_1, reqAddrIn28);
        verifyLeasesChangedCallback(reqAddrIn28Lease);
        mRepo.markLeaseDeclined(declinedAddrIn28);
        mRepo.markLeaseDeclined(declinedFirstAddrIn28);

        // Inside /22, but outside /28:
        final Inet4Address reqAddrIn22 = parseAddr4("192.168.42.3");
        final Inet4Address declinedAddrIn22 = parseAddr4("192.168.42.4");

        final DhcpLease reqAddrIn22Lease = requestLeaseSelecting(TEST_MAC_3, reqAddrIn22);
        verifyLeasesChangedCallback(reqAddrIn28Lease, reqAddrIn22Lease);
        mRepo.markLeaseDeclined(declinedAddrIn22);

        // Address that will be reserved in the updateParams call below
        final Inet4Address reservedAddr = parseAddr4("192.168.42.244");
        final DhcpLease reservedAddrLease = requestLeaseSelecting(TEST_MAC_2, reservedAddr);
        verifyLeasesChangedCallback(reqAddrIn28Lease, reqAddrIn22Lease, reservedAddrLease);

        // Update from /22 to /28 and add another reserved address
        Set<Inet4Address> newReserved = new HashSet<>(TEST_EXCL_SET);
        newReserved.add(reservedAddr);
        mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), newReserved, TEST_LEASE_TIME_MS,
                null /* clientAddr */, DEFAULT_TARGET_PREFIX_LENGTH);
        // Callback is called for the second time with just this lease
        verifyLeasesChangedCallback(2 /* times */, reqAddrIn28Lease);
        verifyNoMoreInteractions(mCallbacks);

        assertHasLease(reqAddrIn28Lease);
        assertDeclined(declinedAddrIn28);

        assertNotDeclined(declinedFirstAddrIn28);

        assertNoLease(reqAddrIn22Lease);
        assertNotDeclined(declinedAddrIn22);

        assertNoLease(reservedAddrLease);
    }

    @Test
    public void testGetOffer_StableAddress() throws Exception {
        for (final MacAddress macAddr : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) {
            final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr,
                    IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);

            // Same lease is offered twice
            final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr,
                    IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
            assertEquals(lease, newLease);
        }
    }

    @SuppressLint("NewApi")
    @Test
    public void testUpdateParams_UsesNewPrefix() throws Exception {
        final IpPrefix newPrefix = new IpPrefix(parseAddr4("192.168.123.0"), 24);
        mRepo.updateParams(newPrefix, TEST_EXCL_SET, TEST_LEASE_TIME_MS, null /* clientAddr */,
                DEFAULT_TARGET_PREFIX_LENGTH);

        DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
                IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
        assertTrue(newPrefix.contains(lease.getNetAddr()));
    }

    @Test
    public void testGetOffer_ExistingLease() throws Exception {
        requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1, TEST_HOSTNAME_1);

        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
                IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
        assertEquals(TEST_INETADDR_1, offer.getNetAddr());
        assertEquals(TEST_HOSTNAME_1, offer.getHostname());
    }

    @Test
    public void testGetOffer_ClientIdHasExistingLease() throws Exception {
        final byte[] clientId = new byte[] { 1, 2 };
        mRepo.requestLease(clientId, TEST_MAC_1, IPV4_ADDR_ANY /* clientAddr */,
                IPV4_ADDR_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false,
                TEST_HOSTNAME_1);

        // Different MAC, but same clientId
        DhcpLease offer = mRepo.getOffer(clientId, TEST_MAC_2,
                IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
        assertEquals(TEST_INETADDR_1, offer.getNetAddr());
        assertEquals(TEST_HOSTNAME_1, offer.getHostname());
    }

    @Test
    public void testGetOffer_DifferentClientId() throws Exception {
        final byte[] clientId1 = new byte[] { 1, 2 };
        final byte[] clientId2 = new byte[] { 3, 4 };
        mRepo.requestLease(clientId1, TEST_MAC_1, IPV4_ADDR_ANY /* clientAddr */,
                IPV4_ADDR_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false,
                TEST_HOSTNAME_1);

        // Same MAC, different client ID
        DhcpLease offer = mRepo.getOffer(clientId2, TEST_MAC_1,
                IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
        // Obtains a different address
        assertNotEquals(TEST_INETADDR_1, offer.getNetAddr());
        assertEquals(HOSTNAME_NONE, offer.getHostname());
        assertEquals(TEST_MAC_1, offer.getHwAddr());
    }

    @Test
    public void testGetOffer_RequestedAddress() throws Exception {
        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, IPV4_ADDR_ANY /* relayAddr */,
                TEST_INETADDR_1 /* reqAddr */, TEST_HOSTNAME_1);
        assertEquals(TEST_INETADDR_1, offer.getNetAddr());
        assertEquals(TEST_HOSTNAME_1, offer.getHostname());

        // Leases are not committed on offer
        verify(mCallbacks, never()).onLeasesChanged(any());
    }

    @Test
    public void testGetOffer_RequestedAddressInUse() throws Exception {
        requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, IPV4_ADDR_ANY /* relayAddr */,
                TEST_INETADDR_1 /* reqAddr */, HOSTNAME_NONE);
        assertNotEquals(TEST_INETADDR_1, offer.getNetAddr());
    }

    @Test
    public void testGetOffer_RequestedAddressReserved() throws Exception {
        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, IPV4_ADDR_ANY /* relayAddr */,
                TEST_RESERVED_ADDR /* reqAddr */, HOSTNAME_NONE);
        assertNotEquals(TEST_RESERVED_ADDR, offer.getNetAddr());
    }

    @Test
    public void testGetOffer_RequestedAddressInvalid() throws Exception {
        final Inet4Address invalidAddr = parseAddr4("192.168.42.0");
        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, IPV4_ADDR_ANY /* relayAddr */,
                invalidAddr /* reqAddr */, HOSTNAME_NONE);
        assertNotEquals(invalidAddr, offer.getNetAddr());
    }

    @Test
    public void testGetOffer_RequestedAddressOutsideSubnet() throws Exception {
        final Inet4Address invalidAddr = parseAddr4("192.168.254.2");
        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, IPV4_ADDR_ANY /* relayAddr */,
                invalidAddr /* reqAddr */, HOSTNAME_NONE);
        assertNotEquals(invalidAddr, offer.getNetAddr());
    }

    @Test
    public void testGetOffer_StaticClientAddress() throws Exception {
        initDhcpLeaseRepositoryWithOption(TEST_CLIENT_ADDR);
        final DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
                IPV4_ADDR_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, TEST_HOSTNAME_1);
        assertEquals(TEST_CLIENT_ADDR, offer.getNetAddr());
        assertEquals(TEST_HOSTNAME_1, offer.getHostname());
    }

    @Test
    public void testGetOffer_StaticClientAddressInUse() throws Exception {
        initDhcpLeaseRepositoryWithOption(TEST_CLIENT_ADDR);
        final byte[] clientId = new byte[] { 1 };
        final DhcpLease lease = mRepo.requestLease(clientId, TEST_MAC_1,
                IPV4_ADDR_ANY /* clientAddr */, IPV4_ADDR_ANY /* relayAddr */,
                TEST_CLIENT_ADDR /* reqAddr */, false, TEST_HOSTNAME_1);

        // Static client address only support single client use case.
        try {
            mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, IPV4_ADDR_ANY /* relayAddr */,
                    INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
            fail("Repository should be out of addresses and throw");
        } catch (DhcpLeaseRepository.OutOfAddressesException e) { /* expected */ }
    }

    @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class)
    public void testGetOffer_RelayInInvalidSubnet() throws Exception {
        mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, parseAddr4("192.168.254.2") /* relayAddr */,
                INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
    }

    @Test
    public void testRequestLease_SelectingTwice() throws Exception {
        final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1,
                TEST_HOSTNAME_1);

        verifyLeasesChangedCallback(lease1);

        // Second request from same client for a different address
        final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_2,
                TEST_HOSTNAME_2);

        verifyLeasesChangedCallback(lease2);

        assertEquals(TEST_INETADDR_1, lease1.getNetAddr());
        assertEquals(TEST_HOSTNAME_1, lease1.getHostname());

        assertEquals(TEST_INETADDR_2, lease2.getNetAddr());
        assertEquals(TEST_HOSTNAME_2, lease2.getHostname());

        // First address freed when client requested a different one: another client can request it
        final DhcpLease lease3 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1, HOSTNAME_NONE);
        assertEquals(TEST_INETADDR_1, lease3.getNetAddr());

        verifyLeasesChangedCallback(lease2, lease3);
        verifyNoMoreInteractions(mCallbacks);
    }

    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
    public void testRequestLease_SelectingInvalid() throws Exception {
        requestLeaseSelecting(TEST_MAC_1, parseAddr4("192.168.254.5"));
    }

    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
    public void testRequestLease_SelectingInUse() throws Exception {
        requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
        requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1);
    }

    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
    public void testRequestLease_SelectingReserved() throws Exception {
        requestLeaseSelecting(TEST_MAC_1, TEST_RESERVED_ADDR);
    }

    @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class)
    public void testRequestLease_SelectingRelayInInvalidSubnet() throws  Exception {
        mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, IPV4_ADDR_ANY /* clientAddr */,
                parseAddr4("192.168.128.1") /* relayAddr */, TEST_INETADDR_1 /* reqAddr */,
                true /* sidSet */, HOSTNAME_NONE);
    }

    @Test
    public void testRequestLease_InitReboot() throws Exception {
        // Request address once
        final DhcpLease oldLease = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
        verifyLeasesChangedCallback(oldLease);

        final long newTime = TEST_TIME + 100;
        when(mClock.elapsedRealtime()).thenReturn(newTime);

        // init-reboot (sidSet == false): verify configuration
        final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_1);
        assertEquals(TEST_INETADDR_1, lease.getNetAddr());
        assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime());

        verifyLeasesChangedCallback(lease);
        verifyNoMoreInteractions(mCallbacks);
    }

    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
    public void testRequestLease_InitRebootWrongAddr() throws Exception {
        // Request address once
        requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
        // init-reboot with different requested address
        requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2);
    }

    @Test
    public void testRequestLease_InitRebootUnknownAddr() throws Exception {
        // init-reboot with unknown requested address
        final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2);
        // RFC2131 says we should not reply to accommodate other servers, but since we are
        // authoritative we allow creating the lease to avoid issues with lost lease DB (same as
        // dnsmasq behavior)
        assertEquals(TEST_INETADDR_2, lease.getNetAddr());
    }

    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
    public void testRequestLease_InitRebootWrongSubnet() throws Exception {
        requestLeaseInitReboot(TEST_MAC_1, parseAddr4("192.168.254.2"));
    }

    @Test
    public void testRequestLease_Renewing() throws Exception {
        final DhcpLease oldLease = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);

        verifyLeasesChangedCallback(oldLease);

        final long newTime = TEST_TIME + 100;
        when(mClock.elapsedRealtime()).thenReturn(newTime);

        final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1);

        assertEquals(TEST_INETADDR_1, lease.getNetAddr());
        assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime());

        verifyLeasesChangedCallback(lease);
        verifyNoMoreInteractions(mCallbacks);
    }

    @Test
    public void testRequestLease_RenewingUnknownAddr() throws Exception {
        final long newTime = TEST_TIME + 100;
        when(mClock.elapsedRealtime()).thenReturn(newTime);
        final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1);
        // Allows renewing an unknown address if available
        assertEquals(TEST_INETADDR_1, lease.getNetAddr());
        assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime());
    }

    @Test
    public void testRequestLease_RenewingAddrInUse() throws Exception {
        final DhcpLease originalLease = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1);
        verifyLeasesChangedCallback(originalLease);

        try {
            requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1);
            fail("Renewing with a different address should fail");
        } catch (DhcpLeaseRepository.InvalidAddressException e) {
            // fall through
        }

        verifyNoMoreInteractions(mCallbacks);
    }

    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
    public void testRequestLease_RenewingInvalidAddr() throws Exception {
        requestLeaseRenewing(TEST_MAC_1, parseAddr4("192.168.254.2"));
    }

    @Test
    public void testReleaseLease() throws Exception {
        final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);

        assertHasLease(lease1);
        assertTrue(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1));
        assertNoLease(lease1);

        final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1);
        assertEquals(TEST_INETADDR_1, lease2.getNetAddr());
    }

    @Test
    public void testReleaseLease_UnknownLease() {
        assertFalse(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1));
    }

    @Test
    public void testReleaseLease_StableOffer() throws Exception {
        for (MacAddress mac : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) {
            final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, mac,
                    IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);

            requestLeaseSelecting(mac, lease.getNetAddr());
            mRepo.releaseLease(CLIENTID_UNSPEC, mac, lease.getNetAddr());

            // Same lease is offered after it was released
            final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, mac,
                    IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
            assertEquals(lease.getNetAddr(), newLease.getNetAddr());
        }
    }

    @Test
    public void testMarkLeaseDeclined() throws Exception {
        final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
                IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);

        mRepo.markLeaseDeclined(lease.getNetAddr());

        // Same lease is not offered again
        final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
                IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
        assertNotEquals(lease.getNetAddr(), newLease.getNetAddr());
    }

    @SuppressLint("NewApi")
    @Test
    public void testMarkLeaseDeclined_UsedIfOutOfAddresses() throws Exception {
        // Use a /28 to quickly run out of addresses
        mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS,
                null /* clientAddr */, DEFAULT_TARGET_PREFIX_LENGTH);

        mRepo.markLeaseDeclined(TEST_INETADDR_1);
        mRepo.markLeaseDeclined(TEST_INETADDR_2);

        // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses
        requestAddresses((byte) 9);

        // Last 2 addresses: addresses marked declined should be used
        final DhcpLease firstLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
                IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_1);
        requestLeaseSelecting(TEST_MAC_1, firstLease.getNetAddr());

        final DhcpLease secondLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2,
                IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_2);
        requestLeaseSelecting(TEST_MAC_2, secondLease.getNetAddr());

        // Now out of addresses
        try {
            mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_3, IPV4_ADDR_ANY /* relayAddr */,
                    INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
            fail("Repository should be out of addresses and throw");
        } catch (DhcpLeaseRepository.OutOfAddressesException e) { /* expected */ }

        assertEquals(TEST_INETADDR_1, firstLease.getNetAddr());
        assertEquals(TEST_HOSTNAME_1, firstLease.getHostname());
        assertEquals(TEST_INETADDR_2, secondLease.getNetAddr());
        assertEquals(TEST_HOSTNAME_2, secondLease.getHostname());
    }

    // Android Lint doesn't handle API change from @SystemApi to public API correctly
    // (see b/193460475). Suppresses NewApi warnings for IpPrefix(InetAddress, int).
    @SuppressLint("NewApi")
    @Test
    public void testSetTargetPrefixLength() throws Exception {
        final Inet4Address p2pStaticAddr = parseAddr4("192.168.49.1");
        final Set<Inet4Address> excludedAddrs = new HashSet<>(Arrays.asList(p2pStaticAddr));
        // Set leasesSubnetPrefixLength to /29.
        mRepo.updateParams(new IpPrefix(p2pStaticAddr, 24), excludedAddrs, TEST_LEASE_TIME_MS,
                null /* clientAddr */, 29);

        // /29 should have 8 addresses (192.168.49.0 ~ 192.168.49.7),
        // 6 w/o the first and last addresses, 5 w/o server addresses: 192.168.49.2 ~ 192.168.49.6.
        final Set<Inet4Address> offerAddresses = requestAddresses((byte) 5);
        assertTrue(offerAddresses.contains(parseAddr4("192.168.49.2")));
        assertTrue(offerAddresses.contains(parseAddr4("192.168.49.3")));
        assertTrue(offerAddresses.contains(parseAddr4("192.168.49.4")));
        assertTrue(offerAddresses.contains(parseAddr4("192.168.49.5")));
        assertTrue(offerAddresses.contains(parseAddr4("192.168.49.6")));

        try {
            mRepo.getOffer(null, TEST_MAC_2,
                    IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
            fail("Should be out of addresses");
        } catch (DhcpLeaseRepository.OutOfAddressesException e) {
            // Expected
        }
    }

    private DhcpLease requestLease(@NonNull MacAddress macAddr, @NonNull Inet4Address clientAddr,
            @Nullable Inet4Address reqAddr, @Nullable String hostname, boolean sidSet)
            throws DhcpLeaseRepository.DhcpLeaseException {
        return mRepo.requestLease(CLIENTID_UNSPEC, macAddr, clientAddr,
                IPV4_ADDR_ANY /* relayAddr */,
                reqAddr, sidSet, hostname);
    }

    /**
     * Request a lease simulating a client in the SELECTING state.
     */
    private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr,
            @NonNull Inet4Address reqAddr, @Nullable String hostname)
            throws DhcpLeaseRepository.DhcpLeaseException {
        return requestLease(macAddr, IPV4_ADDR_ANY /* clientAddr */, reqAddr, hostname,
                true /* sidSet */);
    }

    /**
     * Request a lease simulating a client in the SELECTING state.
     */
    private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr,
            @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException {
        return requestLeaseSelecting(macAddr, reqAddr, HOSTNAME_NONE);
    }

    /**
     * Request a lease simulating a client in the INIT-REBOOT state.
     */
    private DhcpLease requestLeaseInitReboot(@NonNull MacAddress macAddr,
            @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException {
        return requestLease(macAddr, IPV4_ADDR_ANY /* clientAddr */, reqAddr, HOSTNAME_NONE,
                false /* sidSet */);
    }

    /**
     * Request a lease simulating a client in the RENEWING state.
     */
    private DhcpLease requestLeaseRenewing(@NonNull MacAddress macAddr,
            @NonNull Inet4Address clientAddr) throws DhcpLeaseRepository.DhcpLeaseException {
        // Renewing: clientAddr filled in, no reqAddr
        return requestLease(macAddr, clientAddr, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE,
                true /* sidSet */);
    }

    private void assertNoLease(DhcpLease lease) {
        assertFalse("Leases contain " + lease, mRepo.getCommittedLeases().contains(lease));
    }

    private void assertHasLease(DhcpLease lease) {
        assertTrue("Leases do not contain " + lease, mRepo.getCommittedLeases().contains(lease));
    }

    private void assertNotDeclined(Inet4Address addr) {
        assertFalse("Address is declined: " + addr, mRepo.getDeclinedAddresses().contains(addr));
    }

    private void assertDeclined(Inet4Address addr) {
        assertTrue("Address is not declined: " + addr, mRepo.getDeclinedAddresses().contains(addr));
    }

    private void verifyLeasesChangedCallback(int times, DhcpLease... leases) {
        final Set<DhcpLease> expected = new HashSet<>(Arrays.asList(leases));
        try {
            verify(mCallbacks, times(times)).onLeasesChanged(argThat(l ->
                    l.stream().map(DhcpLeaseRepositoryTest::fromParcelable).collect(toSet())
                            .equals(expected)));
        } catch (RemoteException e) {
            fail("Can't happen: " + e);
        }
    }

    private void verifyLeasesChangedCallback(DhcpLease... leases) {
        verifyLeasesChangedCallback(1 /* times */, leases);
    }

    private static DhcpLease fromParcelable(DhcpLeaseParcelable p) {
        return new DhcpLease(
                p.clientId,
                p.hwAddr == null ? null : MacAddress.fromBytes(p.hwAddr),
                intToInet4AddressHTH(p.netAddr),
                p.prefixLength,
                p.expTime,
                p.hostname);
    }
}
