/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.react.uimanager;

import android.util.DisplayMetrics;

import com.facebook.yoga.YogaAlign;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaFlexDirection;
import com.facebook.yoga.YogaJustify;
import com.facebook.yoga.YogaPositionType;
import com.facebook.react.bridge.JavaOnlyMap;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricTestRunner;

import static junit.framework.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyFloat;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mockStatic;

@PrepareForTest({PixelUtil.class})
@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
public class LayoutPropertyApplicatorTest {

  @Rule
  public PowerMockRule rule = new PowerMockRule();

  @Before
  public void setup() {
    DisplayMetricsHolder.setWindowDisplayMetrics(new DisplayMetrics());
    DisplayMetricsHolder.setScreenDisplayMetrics(new DisplayMetrics());
  }

  @After
  public void teardown() {
    DisplayMetricsHolder.setWindowDisplayMetrics(null);
    DisplayMetricsHolder.setScreenDisplayMetrics(null);
  }

  public ReactStylesDiffMap buildStyles(Object... keysAndValues) {
    return new ReactStylesDiffMap(JavaOnlyMap.of(keysAndValues));
  }

  @Test
  public void testDimensions() {
    LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode());
    ReactStylesDiffMap map = spy(
        buildStyles(
            "width",
            10.0,
            "height",
            10.0,
            "left",
            10.0,
            "top",
            10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setStyleWidth(anyFloat());
    verify(map).getFloat(eq("width"), anyFloat());
    verify(reactShadowNode).setStyleHeight(anyFloat());
    verify(map).getFloat(eq("height"), anyFloat());
    verify(reactShadowNode).setPosition(eq(Spacing.START), anyFloat());
    verify(map).getFloat(eq("left"), anyFloat());
    verify(reactShadowNode).setPosition(eq(Spacing.TOP), anyFloat());
    verify(map).getFloat(eq("top"), anyFloat());

    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles());

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode, never()).setStyleWidth(anyFloat());
    verify(map, never()).getFloat(eq("width"), anyFloat());
    verify(reactShadowNode, never()).setStyleHeight(anyFloat());
    verify(map, never()).getFloat(eq("height"), anyFloat());
    verify(reactShadowNode, never()).setPosition(eq(Spacing.START), anyFloat());
    verify(map, never()).getFloat(eq("left"), anyFloat());
    verify(reactShadowNode, never()).setPosition(eq(Spacing.TOP), anyFloat());
    verify(map, never()).getFloat(eq("top"), anyFloat());
  }

  @Test
  public void testFlex() {
    LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode());
    ReactStylesDiffMap map = spy(buildStyles("flex", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setFlex(anyFloat());
    verify(map).getFloat("flex", 0.f);

    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles());

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode, never()).setFlex(anyFloat());
    verify(map, never()).getFloat("flex", 0.f);
  }

  @Test
  public void testPosition() {
    LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode());
    ReactStylesDiffMap map = spy(buildStyles(
        "position",
        "absolute",
        "bottom",
        10.0,
        "right",
        5.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setPosition(eq(Spacing.BOTTOM), anyFloat());
    verify(reactShadowNode).setPosition(eq(Spacing.END), anyFloat());
    verify(reactShadowNode).setPositionType(any(YogaPositionType.class));
    verify(map).getFloat("bottom", Float.NaN);
    verify(map).getFloat("right", Float.NaN);

    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles());

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode, never()).setPosition(eq(Spacing.BOTTOM), anyFloat());
    verify(reactShadowNode, never()).setPosition(eq(Spacing.END), anyFloat());
    verify(reactShadowNode, never()).setPositionType(any(YogaPositionType.class));
    verify(map, never()).getFloat("bottom", Float.NaN);
    verify(map, never()).getFloat("right", Float.NaN);
  }

  @Test
  public void testMargin() {
    // margin
    LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode());
    ReactStylesDiffMap map = spy(buildStyles("margin", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setMargin(eq(Spacing.ALL), anyFloat());
    verify(map).getFloat("margin", YogaConstants.UNDEFINED);

    // marginVertical
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("marginVertical", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setMargin(eq(Spacing.VERTICAL), anyFloat());
    verify(map).getFloat("marginVertical", YogaConstants.UNDEFINED);

    // marginHorizontal
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("marginHorizontal", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setMargin(eq(Spacing.HORIZONTAL), anyFloat());
    verify(map).getFloat("marginHorizontal", YogaConstants.UNDEFINED);

    // marginTop
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("marginTop", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setMargin(eq(Spacing.TOP), anyFloat());
    verify(map).getFloat("marginTop", YogaConstants.UNDEFINED);

    // marginBottom
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("marginBottom", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setMargin(eq(Spacing.BOTTOM), anyFloat());
    verify(map).getFloat("marginBottom", YogaConstants.UNDEFINED);

    // marginLeft
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("marginLeft", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setMargin(eq(Spacing.START), anyFloat());
    verify(map).getFloat("marginLeft", YogaConstants.UNDEFINED);

    // marginRight
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("marginRight", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setMargin(eq(Spacing.END), anyFloat());
    verify(map).getFloat("marginRight", YogaConstants.UNDEFINED);

    // no margin
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles());

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode, never()).setMargin(anyInt(), anyFloat());
    verify(map, never()).getFloat("margin", YogaConstants.UNDEFINED);
  }

  @Test
  public void testPadding() {
    // padding
    LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode());
    ReactStylesDiffMap map = spy(buildStyles("padding", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setPadding(eq(Spacing.ALL), anyFloat());
    verify(map).getFloat("padding", YogaConstants.UNDEFINED);

    // paddingVertical
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("paddingVertical", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setPadding(eq(Spacing.VERTICAL), anyFloat());
    verify(map).getFloat("paddingVertical", YogaConstants.UNDEFINED);

    // paddingHorizontal
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("paddingHorizontal", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setPadding(eq(Spacing.HORIZONTAL), anyFloat());
    verify(map).getFloat("paddingHorizontal", YogaConstants.UNDEFINED);

    // paddingTop
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("paddingTop", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setPadding(eq(Spacing.TOP), anyFloat());
    verify(map).getFloat("paddingTop", YogaConstants.UNDEFINED);

    // paddingBottom
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("paddingBottom", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setPadding(eq(Spacing.BOTTOM), anyFloat());
    verify(map).getFloat("paddingBottom", YogaConstants.UNDEFINED);

    // paddingLeft
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("paddingLeft", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setPadding(eq(Spacing.START), anyFloat());
    verify(map).getFloat("paddingLeft", YogaConstants.UNDEFINED);

    // paddingRight
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles("paddingRight", 10.0));

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setPadding(eq(Spacing.END), anyFloat());
    verify(map).getFloat("paddingRight", YogaConstants.UNDEFINED);

    // no padding
    reactShadowNode = spy(new LayoutShadowNode());
    map = spy(buildStyles());

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode, never()).setPadding(anyInt(), anyFloat());
    verify(map, never()).getFloat("padding", YogaConstants.UNDEFINED);
  }

  @Test
  public void testEnumerations() {
    LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode());
    ReactStylesDiffMap map = buildStyles(
        "flexDirection",
        "column",
        "alignSelf",
        "stretch",
        "alignItems",
        "center",
        "justifyContent",
        "space_between",
        "position",
        "relative");

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setFlexDirection(YogaFlexDirection.COLUMN);
    verify(reactShadowNode).setAlignSelf(YogaAlign.STRETCH);
    verify(reactShadowNode).setAlignItems(YogaAlign.CENTER);
    verify(reactShadowNode).setJustifyContent(YogaJustify.SPACE_BETWEEN);
    verify(reactShadowNode).setPositionType(YogaPositionType.RELATIVE);

    reactShadowNode = spy(new LayoutShadowNode());
    map = buildStyles();
    reactShadowNode.updateProperties(map);

    verify(reactShadowNode, never()).setFlexDirection(any(YogaFlexDirection.class));
    verify(reactShadowNode, never()).setAlignSelf(any(YogaAlign.class));
    verify(reactShadowNode, never()).setAlignItems(any(YogaAlign.class));
    verify(reactShadowNode, never()).setJustifyContent(any(YogaJustify.class));
    verify(reactShadowNode, never()).setPositionType(any(YogaPositionType.class));
  }

  @Test
  public void testPropertiesResetToDefault() {
    DisplayMetrics displayMetrics = new DisplayMetrics();
    displayMetrics.density = 1.0f;
    DisplayMetricsHolder.setWindowDisplayMetrics(displayMetrics);

    LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode());
    ReactStylesDiffMap map = buildStyles(
        "width",
        10.0,
        "height",
        10.0,
        "left",
        10.0,
        "top",
        10.0,
        "flex",
        1.0,
        "padding",
        10.0,
        "marginLeft",
        10.0,
        "borderTopWidth",
        10.0,
        "flexDirection",
        "row",
        "alignSelf",
        "stretch",
        "alignItems",
        "center",
        "justifyContent",
        "space_between",
        "position",
        "absolute");

    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setStyleWidth(10.f);
    verify(reactShadowNode).setStyleHeight(10.f);
    verify(reactShadowNode).setPosition(Spacing.START, 10.f);
    verify(reactShadowNode).setPosition(Spacing.TOP, 10.f);
    verify(reactShadowNode).setFlex(1.0f);
    verify(reactShadowNode).setPadding(Spacing.ALL, 10.f);
    verify(reactShadowNode).setMargin(Spacing.START, 10.f);
    verify(reactShadowNode).setBorder(Spacing.TOP, 10.f);
    verify(reactShadowNode).setFlexDirection(YogaFlexDirection.ROW);
    verify(reactShadowNode).setAlignSelf(YogaAlign.STRETCH);
    verify(reactShadowNode).setAlignItems(YogaAlign.CENTER);
    verify(reactShadowNode).setJustifyContent(YogaJustify.SPACE_BETWEEN);
    verify(reactShadowNode).setPositionType(YogaPositionType.ABSOLUTE);

    map = buildStyles(
        "width",
        null,
        "height",
        null,
        "left",
        null,
        "top",
        null,
        "flex",
        null,
        "padding",
        null,
        "marginLeft",
        null,
        "borderTopWidth",
        null,
        "flexDirection",
        null,
        "alignSelf",
        null,
        "alignItems",
        null,
        "justifyContent",
        null,
        "position",
        null);

    reset(reactShadowNode);
    reactShadowNode.updateProperties(map);
    verify(reactShadowNode).setStyleWidth(YogaConstants.UNDEFINED);
    verify(reactShadowNode).setStyleHeight(YogaConstants.UNDEFINED);
    verify(reactShadowNode).setPosition(Spacing.START, YogaConstants.UNDEFINED);
    verify(reactShadowNode).setPosition(Spacing.TOP, YogaConstants.UNDEFINED);
    verify(reactShadowNode).setFlex(0.f);
    verify(reactShadowNode).setPadding(Spacing.ALL, YogaConstants.UNDEFINED);
    verify(reactShadowNode).setMargin(Spacing.START, YogaConstants.UNDEFINED);
    verify(reactShadowNode).setBorder(Spacing.TOP, YogaConstants.UNDEFINED);
    verify(reactShadowNode).setFlexDirection(YogaFlexDirection.COLUMN);
    verify(reactShadowNode).setAlignSelf(YogaAlign.AUTO);
    verify(reactShadowNode).setAlignItems(YogaAlign.STRETCH);
    verify(reactShadowNode).setJustifyContent(YogaJustify.FLEX_START);
    verify(reactShadowNode).setPositionType(YogaPositionType.RELATIVE);
  }

  @Test
  public void testSettingDefaultStyleValues() {
    mockStatic(PixelUtil.class);
    when(PixelUtil.toPixelFromDIP(anyFloat())).thenAnswer(
        new Answer() {
          @Override
          public Float answer(InvocationOnMock invocation) throws Throwable {
            Object[] args = invocation.getArguments();
            return (Float) args[0];
          }
        });

    LayoutShadowNode[] nodes = new LayoutShadowNode[7];
    for (int idx = 0; idx < nodes.length; idx++) {
      nodes[idx] = new LayoutShadowNode();
      nodes[idx].setDefaultPadding(Spacing.START, 15);
      nodes[idx].setDefaultPadding(Spacing.TOP, 25);
      nodes[idx].setDefaultPadding(Spacing.END, 35);
      nodes[idx].setDefaultPadding(Spacing.BOTTOM, 45);
    }

    ReactStylesDiffMap[] mapNodes = new ReactStylesDiffMap[7];
    mapNodes[0] = buildStyles("paddingLeft", 10.0, "paddingHorizontal", 5.0);
    mapNodes[1] = buildStyles("padding", 10.0, "paddingTop", 5.0);
    mapNodes[2] = buildStyles("paddingLeft", 10.0, "paddingVertical", 5.0);
    mapNodes[3] = buildStyles("paddingBottom", 10.0, "paddingHorizontal", 5.0);
    mapNodes[4] = buildStyles("padding", null, "paddingTop", 5.0);
    mapNodes[5] = buildStyles(
        "paddingRight",
        10.0,
        "paddingHorizontal",
        null,
        "paddingVertical",
        7.0);
    mapNodes[6] = buildStyles("margin", 5.0);

    for (int idx = 0; idx < nodes.length; idx++) {
      nodes[idx].updateProperties(mapNodes[idx]);
    }

    assertEquals(10.0, nodes[0].getPadding(Spacing.START), .0001);
    assertEquals(25.0, nodes[0].getPadding(Spacing.TOP), .0001);
    assertEquals(5.0, nodes[0].getPadding(Spacing.END), .0001);
    assertEquals(45.0, nodes[0].getPadding(Spacing.BOTTOM), .0001);

    assertEquals(10.0, nodes[1].getPadding(Spacing.START), .0001);
    assertEquals(5.0, nodes[1].getPadding(Spacing.TOP), .0001);
    assertEquals(10.0, nodes[1].getPadding(Spacing.END), .0001);
    assertEquals(10.0, nodes[1].getPadding(Spacing.BOTTOM), .0001);

    assertEquals(10.0, nodes[2].getPadding(Spacing.START), .0001);
    assertEquals(5.0, nodes[2].getPadding(Spacing.TOP), .0001);
    assertEquals(35.0, nodes[2].getPadding(Spacing.END), .0001);
    assertEquals(5.0, nodes[2].getPadding(Spacing.BOTTOM), .0001);

    assertEquals(5.0, nodes[3].getPadding(Spacing.START), .0001);
    assertEquals(25.0, nodes[3].getPadding(Spacing.TOP), .0001);
    assertEquals(5.0, nodes[3].getPadding(Spacing.END), .0001);
    assertEquals(10.0, nodes[3].getPadding(Spacing.BOTTOM), .0001);

    assertEquals(15.0, nodes[4].getPadding(Spacing.START), .0001);
    assertEquals(5.0, nodes[4].getPadding(Spacing.TOP), .0001);
    assertEquals(35.0, nodes[4].getPadding(Spacing.END), .0001);
    assertEquals(45.0, nodes[4].getPadding(Spacing.BOTTOM), .0001);

    assertEquals(15.0, nodes[5].getPadding(Spacing.START), .0001);
    assertEquals(7.0, nodes[5].getPadding(Spacing.TOP), .0001);
    assertEquals(10.0, nodes[5].getPadding(Spacing.END), .0001);
    assertEquals(7.0, nodes[5].getPadding(Spacing.BOTTOM), .0001);

    assertEquals(15.0, nodes[6].getPadding(Spacing.START), .0001);
    assertEquals(25.0, nodes[6].getPadding(Spacing.TOP), .0001);
    assertEquals(35.0, nodes[6].getPadding(Spacing.END), .0001);
    assertEquals(45.0, nodes[6].getPadding(Spacing.BOTTOM), .0001);
  }
}
