package com.smartdevicelink.test.transport;

import android.Manifest;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.util.Log;

import com.smartdevicelink.transport.utl.WiFiSocketFactory;

import junit.framework.TestCase;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

import javax.net.SocketFactory;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * This is a unit test class for the WiFiSocketFactory class:
 * {@link com.smartdevicelink.transport.utl.WiFiSocketFactory}
 *
 * Requires LOLLIPOP or later since the tests use android.net.NetworkCapabilities class
 */
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class WiFiSocketFactoryTest extends TestCase {

    private static final String TAG = WiFiSocketFactoryTest.class.getSimpleName();

    private Context mMockContext;
    private PackageManager mMockPackageManager;
    private ConnectivityManager mMockConnMan;
    private SocketFactory mMockSocketFactory; // this is the SocketFactory that creates mWiFiBoundSocket
    private Socket mWiFiBoundSocket;

    private enum FactoryRet {
        RETURNS_NULL,
        RETURNS_CORRECT_FACTORY,
        RETURNS_ANOTHER_FACTORY,
    }

    private class MockNetworkConfig {
        // true to make a null Network
        boolean isNull;
        // specify the transport type of the Network
        int transportType;
        // spcify the type of SocketFactory returned from this Network
        FactoryRet factoryReturnValue;

        MockNetworkConfig(boolean isNull, int transportType, FactoryRet factoryReturnValue) {
            this.isNull = isNull;
            this.transportType = transportType;
            this.factoryReturnValue = factoryReturnValue;
        }
    }

    private void setupMockNetworks(MockNetworkConfig[] configs) {
        if (configs == null) {
            when(mMockConnMan.getAllNetworks()).thenReturn(null);
            return;
        }

        List<Network> networkList = new ArrayList<Network>(configs.length);

        for (MockNetworkConfig config : configs) {
            if (config.isNull) {
                networkList.add(null);
                continue;
            }

            Network network = mock(Network.class);

            NetworkCapabilities networkCapabilities = createNetworkCapabilitiesWithTransport(config.transportType);
            when(mMockConnMan.getNetworkCapabilities(network)).thenReturn(networkCapabilities);

            SocketFactory factory = null;
            switch (config.factoryReturnValue) {
                case RETURNS_NULL:
                    break;
                case RETURNS_CORRECT_FACTORY:
                    factory = mMockSocketFactory;
                    break;
                case RETURNS_ANOTHER_FACTORY:
                    // create another mock SocketFactory instance
                    factory = mock(SocketFactory.class);
                    break;
            }
            when(network.getSocketFactory()).thenReturn(factory);

            networkList.add(network);
        }

        when(mMockConnMan.getAllNetworks()).thenReturn(networkList.toArray(new Network[networkList.size()]));
    }

    private static NetworkCapabilities createNetworkCapabilitiesWithTransport(int transport) {
        // Creates a dummy NetworkCapabilities instance.
        // Since NetworkCapabilities class is 'final', we cannot create its mock. To create a dummy
        // instance, here we use reflection to call its constructor and a method that are marked
        // with "@hide".
        // It is possible that these methods will not be available in a future version of Android.
        // In that case we need to update our code accordingly.
        Class<NetworkCapabilities> c = NetworkCapabilities.class;
        try {
            Method addTransportTypeMethod = c.getMethod("addTransportType", int.class);
            addTransportTypeMethod.setAccessible(true);

            NetworkCapabilities instance = c.getDeclaredConstructor().newInstance();
            addTransportTypeMethod.invoke(instance, transport);
            Log.e(TAG, "Yes successful");
            return instance;
        } catch (Exception e) {
            Log.e(TAG, "Failed to create NetworkCapabilities instance using reflection: ", e);
            return null;
        }
    }

    // from https://stackoverflow.com/questions/40300469/mock-build-version-with-mockito
    // and https://stackoverflow.com/questions/13755117/android-changing-private-static-final-field-using-java-reflection
    private static void setFinalStatic(Field field, Object newValue) throws Exception {
        field.setAccessible(true);
//        Field modifiersField = Field.class.getDeclaredField("modifiers");
        // This call might fail on some devices (for example, Nexus 6 with Android 5.0.1).
        // If that's the issue, we might want to introduce PowerMock.
        Field modifiersField = Field.class.getDeclaredField("accessFlags");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(null, newValue);
    }


    @Override
    public void setUp() throws Exception {
        super.setUp();

        mMockContext = mock(Context.class);
        mMockPackageManager = mock(PackageManager.class);
        mMockConnMan = mock(ConnectivityManager.class);

        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
        when(mMockContext.getPackageName()).thenReturn("dummyPackageName");
        when(mMockContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))).thenReturn(mMockConnMan);

        when(mMockPackageManager.checkPermission(eq(Manifest.permission.ACCESS_NETWORK_STATE), anyString())).thenReturn(PackageManager.PERMISSION_GRANTED);

        mMockSocketFactory = mock(SocketFactory.class);
        mWiFiBoundSocket = new Socket();
        when(mMockSocketFactory.createSocket()).thenReturn(mWiFiBoundSocket);
    }

    @Override
    public void tearDown() throws Exception {
        super.tearDown();
    }

    // test the happy path
    public void testWithWiFiNetwork() {
        setupMockNetworks(new MockNetworkConfig[] {
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_CELLULAR, FactoryRet.RETURNS_ANOTHER_FACTORY),
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY),
        });

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertEquals("Returned Socket should be created through SocketFactory", mWiFiBoundSocket, ret);
    }

    // test the case where SDK_INT is less than 21
    /* This is disabled since Travis CI uses an AVD with 5.1.1 and setFinalStatic() doesn't work on it.
    public void testPriorToLollipop() throws Exception {
        setupMockNetworks(new MockNetworkConfig[] {
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_CELLULAR, FactoryRet.RETURNS_ANOTHER_FACTORY),
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY),
        });

        // simulate SDK_INT to less than LOLLIPOP
        int previousSDKInt = Build.VERSION.SDK_INT;
        setFinalStatic(Build.VERSION.class.getField("SDK_INT"), Build.VERSION_CODES.KITKAT_WATCH);

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        // make sure we revert our change
        setFinalStatic(Build.VERSION.class.getField("SDK_INT"), previousSDKInt);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertNotSame("Returned Socket shouldn't be created through SocketFactory since it is not available prior to LOLLIPOP",
                mWiFiBoundSocket, ret);
    }
    */

    // test the case where we do not have ACCESS_NETWORK_STATE permission
    public void testWithoutPermission() {
        setupMockNetworks(new MockNetworkConfig[] {
            new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY),
        });

        // simulate the case where required permission isn't available
        when(mMockPackageManager.checkPermission(eq(Manifest.permission.ACCESS_NETWORK_STATE), anyString())).thenReturn(PackageManager.PERMISSION_DENIED);

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertNotSame("Returned Socket shouldn't be created through SocketFactory since we don't have required permission",
                mWiFiBoundSocket, ret);
    }

    // test the case where context.getPackageManager() returns null
    public void testPackageManagerNull() {
        setupMockNetworks(new MockNetworkConfig[] {
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY),
        });

        // simulate the case where ConnectivityManager isn't available
        when(mMockContext.getPackageManager()).thenReturn(null);

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertNotSame("Returned Socket shouldn't be created through SocketFactory since PackageManager isn't available",
                mWiFiBoundSocket, ret);
    }

    // test the case where getSystemService() returns null
    public void testConnectivityManagerNull() {
        setupMockNetworks(new MockNetworkConfig[] {
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY),
        });

        // simulate the case where ConnectivityManager isn't available
        when(mMockContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))).thenReturn(null);

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertNotSame("Returned Socket shouldn't be created through SocketFactory since ConnectivityManager isn't working",
                mWiFiBoundSocket, ret);
    }

    // test the case where ConnectivityManager returns null for the network list
    public void testNetworkListNull() {
        setupMockNetworks(null);

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertNotSame("Returned Socket shouldn't be created through SocketFactory since Network list isn't available",
                mWiFiBoundSocket, ret);
    }

    // test the case where the network list contains a null for Network instance
    public void testNetworkListHasNull() {
        setupMockNetworks(new MockNetworkConfig[] {
                // multiple Network instances in the list, the first one being NULL
                new MockNetworkConfig(true, 0, FactoryRet.RETURNS_ANOTHER_FACTORY),
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY),
        });

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertEquals("Returned Socket should be created through SocketFactory", mWiFiBoundSocket, ret);
    }

    // test the case where the phone isn't connected to Wi-Fi network
    public void testNoWiFiNetwork() {
        setupMockNetworks(new MockNetworkConfig[] {
                // none of the instances has TRANSPORT_WIFI in their capabilities
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_CELLULAR, FactoryRet.RETURNS_ANOTHER_FACTORY),
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_BLUETOOTH, FactoryRet.RETURNS_ANOTHER_FACTORY),
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_VPN, FactoryRet.RETURNS_ANOTHER_FACTORY),
        });

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertNotSame("Returned Socket shouldn't be created through SocketFactory since Wi-Fi network isn't available",
                mWiFiBoundSocket, ret);
    }

    // test the case where we get null for SocketFactory
    public void testSocketFactoryNull() {
        setupMockNetworks(new MockNetworkConfig[] {
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_CELLULAR, FactoryRet.RETURNS_ANOTHER_FACTORY),
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_NULL),
        });

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertNotSame("Returned Socket shouldn't be created through SocketFactory since SocketFactory isn't available",
                mWiFiBoundSocket, ret);
    }

    // test the case where we get a null for SocketFactory, then a valid one for another
    public void testSocketFactoryNull2() {
        setupMockNetworks(new MockNetworkConfig[] {
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_CELLULAR, FactoryRet.RETURNS_ANOTHER_FACTORY),
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_NULL),
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY),
        });

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertEquals("Returned Socket should be created through SocketFactory", mWiFiBoundSocket, ret);
    }

    // test the case where we get an exception with SocketFactory.createSocket()
    public void testFactoryReturnsException() throws IOException {
        setupMockNetworks(new MockNetworkConfig[] {
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY),
        });

        when(mMockSocketFactory.createSocket()).thenThrow(new IOException("Dummy IOException for testing!"));

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertNotSame("Returned Socket shouldn't be created through SocketFactory since it throws an IOException",
                mWiFiBoundSocket, ret);
    }

    // Test the case we get multiple Network instances with Wi-Fi transport, and the SocketFactory of
    // the first one throws Exception and the other one succeeds.
    // This is to simulate Samsung Galaxy S9.
    public void testFactoryReturnsException2() throws IOException {
        setupMockNetworks(new MockNetworkConfig[] {
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY),
                new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY),
        });

        when(mMockSocketFactory.createSocket()).thenThrow(new IOException("Dummy IOException for testing!"))
        .thenReturn(mWiFiBoundSocket);

        Socket ret = WiFiSocketFactory.createSocket(mMockContext);

        assertNotNull("createSocket() should always return a Socket instance", ret);
        assertEquals("Returned Socket should be created through SocketFactory", mWiFiBoundSocket, ret);
    }
}