/*
 * Copyright 2012 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.matchers;

import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.util.ASTHelpers.getSymbol;

import com.google.errorprone.BugPattern;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
import com.sun.source.tree.VariableTree;
import javax.lang.model.element.ElementKind;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** {@link CompileTimeConstantExpressionMatcher}Test */
@RunWith(JUnit4.class)
public class CompileTimeConstantExpressionMatcherTest {
  private final CompilationTestHelper testHelper =
      CompilationTestHelper.newInstance(
          CompileTimeConstantExpressionMatcherTester.class, getClass());

  @Test
  public void matchesLiteralsAndStaticFinals() {
    testHelper
        .addSourceLines(
            "Test.java",
            """
            public class Test {
              private final String final_string = "bap";
              private final int final_int = 29;
              private static final int static_final_int = 29;

              public void m() {
                // BUG: Diagnostic contains: true
                String s1 = "boop";
                // BUG: Diagnostic contains: true
                String s2 = "boop" + final_string;
                // BUG: Diagnostic contains: true
                int int2 = 42;
                // BUG: Diagnostic contains: true
                Integer int3 = 42 * final_int;
                // BUG: Diagnostic contains: true
                Integer int4 = 12 - static_final_int;
                // BUG: Diagnostic contains: true
                boolean bool4 = false;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void nullLiteral() {
    testHelper
        .addSourceLines(
            "Test.java",
            """
            public class Test {
              private static final String static_final_string = null;

              public void m() {
                // BUG: Diagnostic contains: true
                String s1 = null;
                // BUG: Diagnostic contains: false
                String s2 = static_final_string;
                // BUG: Diagnostic contains: true
                String s3 = (String) null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void doesNotMatchNonLiterals() {
    testHelper
        .addSourceLines(
            "Test.java",
            """
            package test;

            public class Test {
              private final int nonfinal_int;

              public Test(int i) {
                nonfinal_int = i;
              }

              public void m(String s) {
                // BUG: Diagnostic contains: false
                String s1 = s;
                // BUG: Diagnostic contains: false
                int int2 = s.length();
                // BUG: Diagnostic contains: false
                Integer int3 = nonfinal_int;
                // BUG: Diagnostic contains: false
                Integer int4 = 14 * nonfinal_int;
                // BUG: Diagnostic contains: true
                boolean bool4 = false;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void finalCompileTimeConstantMethodParameters() {
    testHelper
        .addSourceLines(
            "Test.java",
            """
            import com.google.errorprone.annotations.CompileTimeConstant;

            public class Test {
              public void m1(final @CompileTimeConstant String s) {
                // BUG: Diagnostic contains: true
                String s1 = s;
              }

              public void m2(@CompileTimeConstant String s) {
                s = null;
                // BUG: Diagnostic contains: false
                String s2 = s;
              }

              public void m3(final String s) {
                // BUG: Diagnostic contains: false
                String s3 = s;
              }

              public void m4(@CompileTimeConstant String s) {
                // BUG: Diagnostic contains: true
                String s4 = s;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void finalCompileTimeConstantConstructorParameters() {
    testHelper
        .addSourceLines(
            "Test.java",
            """
            import com.google.errorprone.annotations.CompileTimeConstant;

            public class Test {
              public Test(final @CompileTimeConstant String s) {
                // BUG: Diagnostic contains: true
                String s1 = s;
              }

              public Test(@CompileTimeConstant String s, int foo) {
                s = null;
                // BUG: Diagnostic contains: false
                String s2 = s;
              }

              public Test(final String s, boolean foo) {
                // BUG: Diagnostic contains: false
                String s3 = s;
              }

              public Test(@CompileTimeConstant String s, long foo) {
                // BUG: Diagnostic contains: true
                String s4 = s;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void conditionalExpression() {
    testHelper
        .addSourceLines(
            "Test.java",
            """
            abstract class Test {
              abstract boolean g();

              public void m(boolean flag) {
                // BUG: Diagnostic contains: false
                boolean bool1 = flag ? true : g();
                // BUG: Diagnostic contains: false
                boolean bool2 = flag ? g() : false;
                // BUG: Diagnostic contains: true
                boolean bool3 = flag ? true : false;
                // BUG: Diagnostic contains: true
                String str = "a" + (flag ? "b" : "c") + "d";
              }
            }
            """)
        .doTest();
  }

  @Test
  public void parentheses() {
    testHelper
        .addSourceLines(
            "Test.java",
            """
            import com.google.errorprone.annotations.CompileTimeConstant;

            abstract class Test {
              public void m(@CompileTimeConstant String ctc) {
                // BUG: Diagnostic contains: true
                String a = (ctc);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void concatenatedStrings() {
    testHelper
        .addSourceLines(
            "Test.java",
            """
            package test;

            import com.google.errorprone.annotations.CompileTimeConstant;

            abstract class Test {
              public void m(@CompileTimeConstant String ctc, String nonCtc) {
                // BUG: Diagnostic contains: true
                String a = "foo" + ctc;
                // BUG: Diagnostic contains: true
                String b = ctc + "foo";
                // BUG: Diagnostic contains: false
                String c = nonCtc + "foo";
                // BUG: Diagnostic contains: false
                String d = nonCtc + ctc;
                // BUG: Diagnostic contains: true
                String e = "foo" + (ctc == null ? "" : "");
                // BUG: Diagnostic contains: true
                String f = "foo" + 1;
                // BUG: Diagnostic contains: true
                String g = "foo" + 3.14;
              }
            }
            """)
        .doTest();
  }

  /** A test-only bugpattern for testing {@link CompileTimeConstantExpressionMatcher}. */
  @BugPattern(severity = WARNING, summary = "")
  public static final class CompileTimeConstantExpressionMatcherTester extends BugChecker
      implements VariableTreeMatcher {
    @Override
    public Description matchVariable(VariableTree tree, VisitorState state) {
      if (!getSymbol(tree).getKind().equals(ElementKind.LOCAL_VARIABLE)) {
        return Description.NO_MATCH;
      }
      return buildDescription(tree)
          .setMessage(
              CompileTimeConstantExpressionMatcher.instance().matches(tree.getInitializer(), state)
                  ? "true"
                  : "false")
          .build();
    }
  }
}
