/*
 * Copyright 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 com.android.server.wifi.hotspot2;

import static com.android.server.wifi.hotspot2.ServiceProviderVerifier
        .ID_WFA_OID_HOTSPOT_FRIENDLYNAME;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

import android.util.Pair;

import androidx.test.filters.SmallTest;

import com.android.server.wifi.WifiBaseTest;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.x509.GeneralName;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;

import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

/**
 * Unit tests for {@link ServiceProviderVerifier}.
 */
@SmallTest
public class ServiceProviderVerifierTest extends WifiBaseTest {
    private List<List<?>> mNewNames;
    private static final String LOCAL_HOST_NAME = "localhost";
    private static final byte[] LOCAL_HOST_ADDRESS = {127, 0, 0, 1};
    private static final String TEST_FRIENDLY_NAME = "Boingo";
    private static final String TEST_LANGUAGE = "eng";
    private static final Locale TEST_LOCALE = new Locale.Builder().setLanguage(
            TEST_LANGUAGE).build();
    private static final Pair<Locale, String> EXPECTED_RESULT = Pair.create(TEST_LOCALE,
            TEST_FRIENDLY_NAME);
    private static final ASN1ObjectIdentifier WFA_OID_HOTSPOT_FRIENDLYNAME =
            (new ASN1ObjectIdentifier(ID_WFA_OID_HOTSPOT_FRIENDLYNAME));
    private static final int TAG_UTF8STRING = 12;
    @Mock
    private X509Certificate mX509Certificate;

     /**Sets up test. */
    @Before
    public void setUp() throws Exception {
        initMocks(this);
        mNewNames = new ArrayList<>();
    }

    /**
     * Verify that getProviderNames should return empty List in case providerCert is null.
     */
    @Test
    public void testNullForProviderCertShouldReturnEmptyList() {
        assertTrue(ServiceProviderVerifier.getProviderNames(null).isEmpty());
    }

    /**
     * Verify that getProviderNames should return empty List in case providerCert doesn't have
     * SubjectAltName entry
     */
    @Test
    public void testNullFromgetSubjectAlternativeNamesShouldReturnEmptyList() throws Exception {
        when(mX509Certificate.getSubjectAlternativeNames()).thenReturn(null);
        assertTrue(ServiceProviderVerifier.getProviderNames(mX509Certificate).isEmpty());
    }

    /**
     * Verify that getProviderNames should return empty List in case providerCert return empty list
     * for SubjectAltName entry
     */
    @Test
    public void testEmptyListFromGetSubjectAlternativeNamesShouldReturnEmptyList()
            throws Exception {
        when(mX509Certificate.getSubjectAlternativeNames()).thenReturn(Collections.emptySet());
        assertTrue(ServiceProviderVerifier.getProviderNames(mX509Certificate).isEmpty());
    }

    /**
     * Verify that getProviderNames should return empty List in case calling
     * getSubjectAlternativeNames() throws the CertificateParsingException.
     */
    @Test
    public void testExceptionFromGetSubjectAlternativeNamesShouldReturnEmptyList()
            throws Exception {
        doThrow(new CertificateParsingException()).when(
                mX509Certificate).getSubjectAlternativeNames();

        assertTrue(ServiceProviderVerifier.getProviderNames(mX509Certificate).isEmpty());
    }

    /**
     * Verify that getProviderNames should return empty List in case the subjectAlternativeNames
     * doesn't comply with the otherName sequence
     */
    @Test
    public void testNonOtherNameFromGetSubjectAlternativeNamesShouldReturnEmptyList()
            throws Exception {
        mNewNames.add(makeAltNames(new GeneralName(GeneralName.dNSName, LOCAL_HOST_NAME), "DER"));
        mNewNames.add(
                makeAltNames(new GeneralName(GeneralName.iPAddress,
                        new DEROctetString(LOCAL_HOST_ADDRESS)), "DER"));
        when(mX509Certificate.getSubjectAlternativeNames()).thenReturn(
                Collections.unmodifiableCollection(mNewNames));

        assertTrue(ServiceProviderVerifier.getProviderNames(mX509Certificate).isEmpty());
    }

    /**
     * Verify that getProviderNames should return empty List in case the subjectAlternativeNames
     * returns the result that has only one element in the list.
     */
    @Test
    public void testInvalidFormatFromGetSubjectAlternativeNamesShouldReturnEmptyList()
            throws Exception {
        // Create a list that has one element as result for getSubjectAlternativeNames()
        // to violate the expected format that has two elements in a list.
        List<Object> nameEntry = new ArrayList<>(1);
        nameEntry.add(Integer.valueOf(4));
        mNewNames.add(nameEntry);
        when(mX509Certificate.getSubjectAlternativeNames()).thenReturn(
                Collections.unmodifiableCollection(mNewNames));

        assertTrue(ServiceProviderVerifier.getProviderNames(mX509Certificate).isEmpty());
    }

    /**
     * Verify that getProviderNames should return a List that has a result in case the
     * subjectAlternativeNames has valid friendly name for service provider.
     */
    @Test
    public void testValidEntryFromGetSubjectAlternativeNamesShouldReturnList()
            throws Exception {
        // Create the valid entry for FriendlyName
        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(WFA_OID_HOTSPOT_FRIENDLYNAME);
        v.add(new DERTaggedObject(TAG_UTF8STRING,
                new DERUTF8String(TEST_LANGUAGE + TEST_FRIENDLY_NAME)));
        mNewNames.add(
                makeAltNames(new GeneralName(GeneralName.otherName, new DERSequence(v)), "DER"));

        when(mX509Certificate.getSubjectAlternativeNames()).thenReturn(
                Collections.unmodifiableCollection(mNewNames));

        List<Pair<Locale, String>> result = ServiceProviderVerifier.getProviderNames(
                mX509Certificate);

        assertThat(result.size(), is(1));
        assertEquals(EXPECTED_RESULT, result.get(0));
    }

    /**
     * Verify that verifyCertFingerPrint should return {@code true} when a fingerprint of {@link
     * X509Certificate} is same with a value of hash provided.
     */
    @Test
    public void testVerifyFingerPrintOfCertificateWithSameFingerPrintValueReturnTrue()
            throws Exception {
        String testData = "testData";
        String testHash = "ba477a0ac57e10dd90bb5bf0289c5990fe839c619b26fde7c2aac62f526d4113";
        when(mX509Certificate.getEncoded()).thenReturn(testData.getBytes());

        assertTrue(ServiceProviderVerifier.verifyCertFingerprint(mX509Certificate,
                hexToBytes(testHash)));
    }

    /**
     * Verify that verifyCertFingerPrint should return {@code false} when a fingerprint of {@link
     * X509Certificate} is different with a value of hash provided.
     */
    @Test
    public void testVerifyFingerPrintOfCertificateWithDifferentFingerPrintValueReturnFalse()
            throws Exception {
        String testData = "differentData";
        String testHash = "ba477a0ac57e10dd90bb5bf0289c5990fe839c619b26fde7c2aac62f526d4113";
        when(mX509Certificate.getEncoded()).thenReturn(testData.getBytes());

        assertFalse(ServiceProviderVerifier.verifyCertFingerprint(mX509Certificate,
                hexToBytes(testHash)));
    }

    /**
     * Helper function to create an entry complying with the format returned
     * {@link X509Certificate#getSubjectAlternativeNames()}
     */
    private List<Object> makeAltNames(GeneralName name, String encoding) throws Exception {
        List<Object> nameEntry = new ArrayList<>(2);
        nameEntry.add(Integer.valueOf(name.getTagNo()));
        nameEntry.add(name.getEncoded(encoding));

        return nameEntry;
    }

    /**
     * Converts a hex string to an array of bytes. The {@code hex} should have an even length. If
     * not, the last character will be ignored.
     */
    private byte[] hexToBytes(String hex) {
        byte[] output = new byte[hex.length() / 2];
        for (int i = 0, j = 0; i + 1 < hex.length(); i += 2, j++) {
            output[j] = (byte) (charToByte(hex.charAt(i)) << 4 | charToByte(hex.charAt(i + 1)));
        }
        return output;
    }

    /**
     * Converts a character of [0-9a-aA-F] to its hex value in a byte. If the character is not a
     * hex number, 0 will be returned.
     */
    private byte charToByte(char c) {
        if (c >= 0x30 && c <= 0x39) {
            return (byte) (c - 0x30);
        } else if (c >= 0x41 && c <= 0x46) {
            return (byte) (c - 0x37);
        } else if (c >= 0x61 && c <= 0x66) {
            return (byte) (c - 0x57);
        }
        return 0;
    }
}
