/*
 * Copyright 2014 The Error Prone Authors.
 *
 * 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.google.errorprone.bugpatterns;

import com.google.errorprone.CompilationTestHelper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * @author cushon@google.com (Liam Miller-Cushon)
 */
@RunWith(JUnit4.class)
public class NarrowingCompoundAssignmentTest {
  private final CompilationTestHelper compilationHelper =
      CompilationTestHelper.newInstance(NarrowingCompoundAssignment.class, getClass());

  @Test
  public void positiveCase() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    short s = 0;",
            "    char t = 0;",
            "    byte u = 0;",
            "    float v = 0;",
            "    // BUG: Diagnostic contains: s = (short) (s * 1)",
            "    s *= 1;",
            "    // BUG: Diagnostic contains: t = (char) (t * 1)",
            "    t *= 1;",
            "    // BUG: Diagnostic contains: u = (byte) (u * 1)",
            "    u *= 1;",
            "    // BUG: Diagnostic contains: u = (byte) (u * 1L)",
            "    u *= 1L;",
            "    // BUG: Diagnostic contains: v = (float) (v * 1.0)",
            "    v *= 1.0;",
            "    // BUG: Diagnostic contains: v = (float) (v * 1.0d)",
            "    v *= 1.0d;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void allOps() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    short s = 0;",
            "    // BUG: Diagnostic contains: s = (short) (s * 1)",
            "    s *= 1;",
            "    // BUG: Diagnostic contains: s = (short) (s / 1)",
            "    s /= 1;",
            "    // BUG: Diagnostic contains: s = (short) (s % 1)",
            "    s %= 1;",
            "    // BUG: Diagnostic contains: s = (short) (s + 1)",
            "    s += 1;",
            "    // BUG: Diagnostic contains: s = (short) (s - 1)",
            "    s -= 1;",
            "    // BUG: Diagnostic contains: s = (short) (s << 1)",
            "    s <<= 1;",
            "    // Signed right shifts are OK",
            "    s >>= 1;",
            "    // BUG: Diagnostic contains: s = (short) (s >>> 1)",
            "    s >>>= 1;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void deficientRightShift() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    // BUG: Diagnostic contains: i = (short) (i >>> 1)",
            "    for (short i = -1; i != 0; i >>>= 1);",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void negativeCase() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    int s = 0;",
            "    long t = 0;",
            "    double u = 0;",
            "    s *= 1;",
            "    t *= 1;",
            "    u *= 1;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void floatFloat() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    float a = 0;",
            "    float b = 0;",
            "    Float c = Float.valueOf(0);",
            "    a += b;",
            "    a += c;",
            "  }",
            "}")
        .doTest();
  }

  // bit twiddling deficient types with masks of the same width is fine
  @Test
  public void bitTwiddle() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    short smask = 0b1;",
            "    byte bmask = 0b1;",
            "",
            "    short s = 0;",
            "    byte b = 0;",
            "",
            "    s &= smask;",
            "    s |= smask;",
            "    s ^= smask;",
            "",
            "    s &= bmask;",
            "    s |= bmask;",
            "    s ^= bmask;",
            "",
            "    b &= bmask;",
            "    b |= bmask;",
            "    b ^= bmask;",
            "  }",
            "}")
        .doTest();
  }

  // bit twiddling deficient types with constant masks of the same width is fine
  @Test
  public void bitTwiddleConstant() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    short s = 0;",
            "    byte b = 0;",
            "",
            "    s &= ~1;",
            "    s |= 1;",
            "    s ^= 1;",
            "",
            "    b &= ~1;",
            "    b |= 1;",
            "    b ^= 1;",
            "",
            "    b |= 128;",
            "    b &= 128;",
            "    b ^= 128;",
            "    b |= 1L;",
            "    b &= 1L;",
            "    b ^= 1L;",
            "",
            "    // BUG: Diagnostic contains: b = (byte) (b | 256)",
            "    b |= 256;",
            "    // BUG: Diagnostic contains: b = (byte) (b & ~256)",
            "    b &= ~256;",
            "    // BUG: Diagnostic contains: b = (byte) (b ^ 256)",
            "    b ^= 256;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void allowsBinopsOfDeficientTypes() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    short smask = 0b1;",
            "    byte bmask = 0b1;",
            "",
            "    short s = 0;",
            "    byte b = 0;",
            "",
            "    s += smask;",
            "    s -= smask;",
            "    s *= smask;",
            "",
            "    s += bmask;",
            "    s -= bmask;",
            "    s *= bmask;",
            "",
            "    b -= bmask;",
            "    b += bmask;",
            "    b /= bmask;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void preservePrecedence() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    float f = 0;",
            "    // BUG: Diagnostic contains: f = (float) (f - (3.0 - 2.0))",
            "    f -= 3.0 - 2.0;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void preservePrecedence2() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    float f = 0;",
            "    // BUG: Diagnostic contains: f = (float) (f - 3.0 * 2.0)",
            "    f -= 3.0 * 2.0;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void preservePrecedence3() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    float f = 0;",
            "    // BUG: Diagnostic contains: f = (float) (f - (3.0 > 0 ? 1.0 : 2.0))",
            "    f -= 3.0 > 0 ? 1.0 : 2.0;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void preservePrecedenceExhaustive_multMult() throws Exception {
    testPrecedence("*", "*", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_multPlus() throws Exception {
    testPrecedence("*", "+", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_multLShift() throws Exception {
    testPrecedence("*", "<<", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_multAnd() throws Exception {
    testPrecedence("*", "&", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_multOr() throws Exception {
    testPrecedence("*", "|", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_plusMult() throws Exception {
    testPrecedence("+", "*", /* parens= */ false);
  }

  @Test
  public void preservePrecedenceExhaustive_plusPlus() throws Exception {
    testPrecedence("+", "+", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_plusLShift() throws Exception {
    testPrecedence("+", "<<", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_plusAnd() throws Exception {
    testPrecedence("+", "&", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_plusOr() throws Exception {
    testPrecedence("+", "|", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_lShiftMult() throws Exception {
    testPrecedence("<<", "*", /* parens= */ false);
  }

  @Test
  public void preservePrecedenceExhaustive_lShiftPlus() throws Exception {
    testPrecedence("<<", "+", /* parens= */ false);
  }

  @Test
  public void preservePrecedenceExhaustive_lShiftLShift() throws Exception {
    testPrecedence("<<", "<<", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_lShiftAnd() throws Exception {
    testPrecedence("<<", "&", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_lShiftOr() throws Exception {
    testPrecedence("<<", "|", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_andMult() throws Exception {

    testPrecedence("&", "*", /* parens= */ false);
  }

  @Test
  public void preservePrecedenceExhaustive_andPlus() throws Exception {
    testPrecedence("&", "+", /* parens= */ false);
  }

  @Test
  public void preservePrecedenceExhaustive_andLShift() throws Exception {
    testPrecedence("&", "<<", /* parens= */ false);
  }

  @Test
  public void preservePrecedenceExhaustive_andAnd() throws Exception {
    testPrecedence("&", "&", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_andOr() throws Exception {
    testPrecedence("&", "|", /* parens= */ true);
  }

  @Test
  public void preservePrecedenceExhaustive_orMult() throws Exception {
    testPrecedence("|", "*", /* parens= */ false);
  }

  @Test
  public void preservePrecedenceExhaustive_orPlus() throws Exception {
    testPrecedence("|", "+", /* parens= */ false);
  }

  @Test
  public void preservePrecedenceExhaustive_orLShift() throws Exception {
    testPrecedence("|", "<<", /* parens= */ false);
  }

  @Test
  public void preservePrecedenceExhaustive_orAnd() throws Exception {
    testPrecedence("|", "&", /* parens= */ false);
  }

  @Test
  public void preservePrecedenceExhaustive_orOr() throws Exception {
    testPrecedence("|", "|", /* parens= */ true);
  }

  private void testPrecedence(String opA, String opB, boolean parens) {
    String rhs = String.format("100_000 %s 200_000", opB);
    if (parens) {
      rhs = "(" + rhs + ")";
    }
    String expect = String.format("s = (short) (s %s %s", opA, rhs);

    String compoundAssignment = String.format("    s %s= 100_000 %s 200_000;", opA, opB);

    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    short s = 0;",
            "    // BUG: Diagnostic contains: " + expect,
            compoundAssignment,
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void doubleLong() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    long a = 1;",
            "    double b = 2;",
            "    // BUG: Diagnostic contains: Compound assignments from double to long",
            "    a *= b;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void doubleInt() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    int a = 1;",
            "    double b = 2;",
            "    // BUG: Diagnostic contains: Compound assignments from double to int",
            "    a *= b;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void floatLong() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    long a = 1;",
            "    float b = 2;",
            "    // BUG: Diagnostic contains: Compound assignments from float to long",
            "    a *= b;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void floatInt() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    int a = 1;",
            "    float b = 2;",
            "    // BUG: Diagnostic contains:" + " Compound assignments from float to int",
            "    a *= b;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void exhaustiveTypes() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void f(short s, byte b, char c, int i, long l, float f, double d) {",
            "    s += s;",
            "    s += b;",
            "    // BUG: Diagnostic contains:",
            "    s += c;",
            "    // BUG: Diagnostic contains:",
            "    s += i;",
            "    // BUG: Diagnostic contains:",
            "    s += l;",
            "    // BUG: Diagnostic contains:",
            "    s += f;",
            "    // BUG: Diagnostic contains:",
            "    s += d;",
            "    // BUG: Diagnostic contains:",
            "    b += s;",
            "    b += b;",
            "    // BUG: Diagnostic contains:",
            "    b += c;",
            "    // BUG: Diagnostic contains:",
            "    b += i;",
            "    // BUG: Diagnostic contains:",
            "    b += l;",
            "    // BUG: Diagnostic contains:",
            "    b += f;",
            "    // BUG: Diagnostic contains:",
            "    b += d;",
            "    // BUG: Diagnostic contains:",
            "    c += s;",
            "    // BUG: Diagnostic contains:",
            "    c += b;",
            "    c += c;",
            "    // BUG: Diagnostic contains:",
            "    c += i;",
            "    // BUG: Diagnostic contains:",
            "    c += l;",
            "    // BUG: Diagnostic contains:",
            "    c += f;",
            "    // BUG: Diagnostic contains:",
            "    c += d;",
            "    i += s;",
            "    i += b;",
            "    i += c;",
            "    i += i;",
            "    // BUG: Diagnostic contains:",
            "    i += l;",
            "    // BUG: Diagnostic contains:",
            "    i += f;",
            "    // BUG: Diagnostic contains:",
            "    i += d;",
            "    l += s;",
            "    l += b;",
            "    l += c;",
            "    l += i;",
            "    l += l;",
            "    // BUG: Diagnostic contains:",
            "    l += f;",
            "    // BUG: Diagnostic contains:",
            "    l += d;",
            "    f += s;",
            "    f += b;",
            "    f += c;",
            "    f += i;",
            "    f += l;",
            "    f += f;",
            "    // BUG: Diagnostic contains:",
            "    f += d;",
            "    d += s;",
            "    d += b;",
            "    d += c;",
            "    d += i;",
            "    d += l;",
            "    d += f;",
            "    d += d;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void boxing() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    int a = 1;",
            "    // BUG: Diagnostic contains: from Long to int",
            "    a += (Long) 0L;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void stringConcat() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "class Test {",
            "  void m() {",
            "    String a = \"\";",
            "    a += (char) 0;",
            "  }",
            "}")
        .doTest();
  }
}
