package com.avg.zaap.crashlibrary.provider.device;

import android.annotation.SuppressLint;
import android.os.Build;

import com.avg.zaap.crashlibrary.core.writer.DataWriterException;
import com.avg.zaap.crashlibrary.core.writer.IDataWriter;
import com.avg.zaap.crashlibrary.provider.CrashProviderException;
import com.avg.zaap.crashlibrary.provider.ICrashAsyncProvider;
import com.avg.zaap.crashlibrary.provider.ICrashProviderCallback;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;

import java.io.File;

import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

/**
 * A test class for {@link DeviceInfoProvider} class.
 * <p>
 * Created by marek.osvald on 28.4.2016.
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest({Build.class, Build.VERSION.class})
public class DeviceInfoProviderTests {

    private static final String PROVIDER_NAME = DeviceInfoProvider.class.getSimpleName();
    @SuppressLint("SdCardPath")
    private static final String DIRECTORY = "/sdcard/dump";
    private static final String FILE_NAME = "DeviceInfo.txt";
    private static final String FILE_PATH = DIRECTORY + File.separatorChar + FILE_NAME;

    //@formatter:off
    private static final String DEVICE_INFO_PRE_LOLLIPOP =
            "Board:              mockBoard\n" +
            "Bootloader:         mockBootloader\n" +
            "Brand:              mockBrand\n" +
            "Device:             mockDevice\n" +
            "Display:            mockDisplay\n" +
            "HW:                 mockHardware\n" +
            "Host:               mockHost\n" +
            "ID:                 mockId\n" +
            "Manufacturer:       mockManufacturer\n" +
            "Model:              mockModel\n" +
            "Product:            mockProduct\n" +
            "SDK:                " + (Build.VERSION_CODES.LOLLIPOP - 1) + "\n" +
            "Serial:             mockSerial\n" +
            "Tags:               mockTags\n" +
            "Type:               mockType\n" +
            "User:               mockUser\n" +
            "ABIs:               mockCpuAbi, mockCpuAbi2\n";

    private static final String DEVICE_INFO_POST_LOLLIPOP =
            "Board:              mockBoard\n" +
            "Bootloader:         mockBootloader\n" +
            "Brand:              mockBrand\n" +
            "Device:             mockDevice\n" +
            "Display:            mockDisplay\n" +
            "HW:                 mockHardware\n" +
            "Host:               mockHost\n" +
            "ID:                 mockId\n" +
            "Manufacturer:       mockManufacturer\n" +
            "Model:              mockModel\n" +
            "Product:            mockProduct\n" +
            "SDK:                " + (Build.VERSION_CODES.LOLLIPOP) + "\n" +
            "Serial:             mockSerial\n" +
            "Tags:               mockTags\n" +
            "Type:               mockType\n" +
            "User:               mockUser\n" +
            "ABIs:               mockAbi, mockAbi2, mockAbi3\n";
    //@formatter:on

    private ICrashAsyncProvider mDeviceInfoProvider;
    private IDataWriter mDataWriter;
    private ICrashProviderCallback mCrashProviderCallback;
    private ArgumentCaptor<String> mProviderNameCaptor;
    private ArgumentCaptor<Throwable> mThrowableCaptor;

    @Before
    public void setUp() throws Exception {
        mDataWriter = mock(IDataWriter.class);
        mCrashProviderCallback = mock(ICrashProviderCallback.class);

        mDeviceInfoProvider = new DeviceInfoProvider(new IDeviceInfoProviderFactory() {
            @Override
            public IDataWriter getDataWriter() {
                return mDataWriter;
            }
        });

        //@formatter:off
        PowerMockito.mockStatic(Build.class);
        Whitebox.setInternalState(Build.class, "BOARD",         "mockBoard");
        Whitebox.setInternalState(Build.class, "BOOTLOADER",    "mockBootloader");
        Whitebox.setInternalState(Build.class, "BRAND",         "mockBrand");
        Whitebox.setInternalState(Build.class, "DEVICE",        "mockDevice");
        Whitebox.setInternalState(Build.class, "DISPLAY",       "mockDisplay");
        Whitebox.setInternalState(Build.class, "HARDWARE",      "mockHardware");
        Whitebox.setInternalState(Build.class, "HOST",          "mockHost");
        Whitebox.setInternalState(Build.class, "ID",            "mockId");
        Whitebox.setInternalState(Build.class, "MANUFACTURER",  "mockManufacturer");
        Whitebox.setInternalState(Build.class, "MODEL",         "mockModel");
        Whitebox.setInternalState(Build.class, "PRODUCT",       "mockProduct");
        Whitebox.setInternalState(Build.class, "SERIAL",        "mockSerial");
        Whitebox.setInternalState(Build.class, "TAGS",          "mockTags");
        Whitebox.setInternalState(Build.class, "TYPE",          "mockType");
        Whitebox.setInternalState(Build.class, "USER",          "mockUser");
        //noinspection SpellCheckingInspection
        Whitebox.setInternalState(Build.class, "SUPPORTED_ABIS", new String[] {"mockAbi", "mockAbi2", "mockAbi3"});
        Whitebox.setInternalState(Build.class, "CPU_ABI",        "mockCpuAbi");
        Whitebox.setInternalState(Build.class, "CPU_ABI2",       "mockCpuAbi2");
        //@formatter:on

        simulateBuild(AndroidBuild.PRE_LOLLIPOP);
        initializeDataWriter(Result.OK);
        mProviderNameCaptor = ArgumentCaptor.forClass(String.class);
        mThrowableCaptor = ArgumentCaptor.forClass(Throwable.class);
    }

    @Test
    public void provideData_deviceBeforeLollipopWriterOK_callsOnSuccess() throws Exception {
        // act
        mDeviceInfoProvider.provideData(DIRECTORY, mCrashProviderCallback);

        // assert
        verify(mDataWriter, times(1)).write(FILE_PATH, DEVICE_INFO_PRE_LOLLIPOP);
        verify(mCrashProviderCallback, times(1)).onSuccess(FILE_PATH);
        verify(mCrashProviderCallback, never()).onFailure(anyString(), any(Throwable.class));
    }

    @Test
    public void provideData_deviceLollipopWriterOK_callsOnSuccess() throws Exception {
        // arrange
        simulateBuild(AndroidBuild.POST_LOLLIPOP);

        // act
        mDeviceInfoProvider.provideData(DIRECTORY, mCrashProviderCallback);

        // assert
        verify(mDataWriter, times(1)).write(FILE_PATH, DEVICE_INFO_POST_LOLLIPOP);
        verify(mCrashProviderCallback, times(1)).onSuccess(FILE_PATH);
        verify(mCrashProviderCallback, never()).onFailure(anyString(), any(Throwable.class));
    }

    @Test
    public void provideData_deviceBeforeLollipopWriterFails_callsOnFailure() throws Exception {
        // arrange
        initializeDataWriter(Result.EXCEPTION);

        // act
        mDeviceInfoProvider.provideData(DIRECTORY, mCrashProviderCallback);

        // assert
        verify(mDataWriter, times(1)).write(FILE_PATH, DEVICE_INFO_PRE_LOLLIPOP);
        verify(mCrashProviderCallback, never()).onSuccess(anyString());
        verify(mCrashProviderCallback, times(1)).onFailure(mProviderNameCaptor.capture(), mThrowableCaptor.capture());
        assertThat(mProviderNameCaptor.getValue(), is(PROVIDER_NAME));
        assertThat(mThrowableCaptor.getValue(), instanceOf(CrashProviderException.class));
    }

    @Test
    public void provideData_deviceLollipopWriterFails_callsOnFailure() throws Exception {
        // arrange
        simulateBuild(AndroidBuild.POST_LOLLIPOP);
        initializeDataWriter(Result.EXCEPTION);

        // act
        mDeviceInfoProvider.provideData(DIRECTORY, mCrashProviderCallback);

        // assert
        verify(mDataWriter, times(1)).write(FILE_PATH, DEVICE_INFO_POST_LOLLIPOP);
        verify(mCrashProviderCallback, never()).onSuccess(anyString());
        verify(mCrashProviderCallback, times(1)).onFailure(mProviderNameCaptor.capture(), mThrowableCaptor.capture());
        assertThat(mProviderNameCaptor.getValue(), is(PROVIDER_NAME));
        assertThat(mThrowableCaptor.getValue(), instanceOf(CrashProviderException.class));
    }

    private void initializeDataWriter(Result result) throws Exception {
        switch (result) {
            case OK:
                // this case doesn't really do anything, but it's here if ever necessary
                doNothing().when(mDataWriter).write(FILE_PATH, DEVICE_INFO_PRE_LOLLIPOP);
                doNothing().when(mDataWriter).write(FILE_PATH, DEVICE_INFO_POST_LOLLIPOP);
                break;

            case EXCEPTION:
                doThrow(new DataWriterException()).when(mDataWriter).write(FILE_PATH, DEVICE_INFO_PRE_LOLLIPOP);
                doThrow(new DataWriterException()).when(mDataWriter).write(FILE_PATH, DEVICE_INFO_POST_LOLLIPOP);
                break;

            default:
                fail();
        }
    }

    private void simulateBuild(AndroidBuild build) {
        switch (build) {
            case PRE_LOLLIPOP:
                Whitebox.setInternalState(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP - 1);
                break;

            case POST_LOLLIPOP:
                Whitebox.setInternalState(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP);
                break;

            default:
                fail();
        }
    }

    enum AndroidBuild {
        PRE_LOLLIPOP, POST_LOLLIPOP
    }

    enum Result {
        OK, EXCEPTION
    }

}
