/*
 * Copyright 2006 The Closure Compiler 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.javascript.jscomp;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit tests for {@link ExploitAssigns} */
@RunWith(JUnit4.class)
public final class ExploitAssignsTest extends CompilerTestCase {

  @Test
  public void testExprExploitationTypes() {
    test("a = true; b = true", "b = a = true");
    test("a = !0; b = !0", "b = a = !0");
    test("a = !1; b = !1", "b = a = !1");
    test("a = void 0; b = void 0", "b = a = void 0");
    test("a = -Infinity; b = -Infinity", "b = a = -Infinity");
  }

  @Test
  public void testExprExploitationTypes2() {
    test("a = !0; b = !0", "b = a = !0");
  }

  @Test
  public void nullishCoalesce() {
    test("a = null; a ?? b;", "(a = null)??b");
    test("a = true; if (a ?? a) { foo(); }", "if ((a = true) ?? a) { foo() }");
    test("a = !0; if (a ?? a) { foo(); }", "if ((a = !0) ?? a) { foo() }");
  }

  @Test
  public void testExprExploitation() {
    test("a = null; b = null; var c = b", "var c = b = a = null");
    test("a = null; b = null", "b = a = null");
    test("a = undefined; b = undefined", "b = a = undefined");
    test("a = 0; b = 0", "b=a=0");
    test("a = 'foo'; b = 'foo'", "b = a = \"foo\"");
    test("a = c; b = c", "b=a=c");

    testSame("a = 0; b = 1");
    testSame("a = \"foo\"; b = \"foox\"");

    test("a = null; a && b;", "(a = null)&&b");
    test("a = null; a || b;", "(a = null)||b");

    test("a = null; a ? b : c;", "(a = null) ? b : c");

    test("a = null; this.foo = null;", "this.foo = a = null");
    test("function f(){ a = null; return null; }", "function f(){return a = null}");

    test("a = true; if (a) { foo(); }", "if (a = true) { foo() }");
    test("a = true; if (a && a) { foo(); }", "if ((a = true) && a) { foo() }");
    test("a = false; if (a) { foo(); }", "if (a = false) { foo() }");

    test("a = !0; if (a) { foo(); }", "if (a = !0) { foo() }");
    test("a = !0; if (a && a) { foo(); }", "if ((a = !0) && a) { foo() }");
    test("a = !1; if (a) { foo(); }", "if (a = !1) { foo() }");

    testSame("a = this.foo; a();");
    test("a = b; b = a;", "b = a = b");
    testSame("a = b; a.c = a");
    test("this.foo = null; this.bar = null;", "this.bar = this.foo = null");
    test(
        "this.foo = null; this.bar = null; this.baz = this.bar",
        "this.baz = this.bar = this.foo = null");
    test(
        "this.foo = null; this.bar = null; this.baz = this?.bar",
        "this.bar = this.foo = null; this.baz = this?.bar;");
    test("this.foo = null; a = null;", "a = this.foo = null");
    test("this.foo = null; a = this.foo;", "a = this.foo = null");
    testSame("this.foo = null; a = this?.foo;");
    test("a.b.c=null; a=null;", "a = a.b.c = null");
    testSame("a = null; a.b.c = null");
    test("(a=b).c = null; this.b = null;", "this.b = (a=b).c = null");
    testSame("if(x) a = null; else b = a");
  }

  @Test
  public void testLetConstAssignment() {
    test("a = null; b = null; let c = b", "let c = b = a = null");
  }

  @Test
  public void testBlockScope() {
    test("{ a = null; b = null; c = b }", "{ c = b = a = null }");

    // TODO (simranarora) What should we have as the intended behavior with block scoping?
    test(
        "a = null; b = null; { c = b; }",
        // "{ c = b = a = null; }
        "b = a = null; { c = b; }");
  }

  @Test
  public void testExploitInArrowFunction() {
    test("() => { a = null; return null; }", "() => { return a = null }");
  }

  @Test
  public void testNestedExprExploitation() {
    test(
        "this.foo = null; this.bar = null; this.baz = null;",
        "this.baz = this.bar = this.foo = null");

    test(
        "a = 3; this.foo = a; this.bar = a; this.baz = 3;",
        "this.baz = this.bar = this.foo = a = 3");
    test(
        "a = 3; this.foo = a; this.bar = this.foo; this.baz = a;",
        "this.baz = this.bar = this.foo = a = 3");
    // recursively optimize assigns until optional chaining on RHS
    test(
        "a = 3; this.foo = a; this.bar = this?.foo; this.baz = a;",
        "this.foo = a = 3; this.bar = this?.foo; this.baz = a;");
    test(
        "a = 3; this.foo = a; this.bar = 3; this.baz = this.foo;",
        "this.baz = this.bar = this.foo = a = 3");
    // recursively optimize assigns until optional chaining on RHS
    test(
        "a = 3; this.foo = a; this.bar = 3; this.baz = this?.foo;",
        "this.bar = this.foo = a = 3; this.baz = this?.foo;");
    test(
        "a = 3; this.foo = a; a = 3; this.bar = 3; " + "a = 3; this.baz = this.foo;",
        "this.baz = a = this.bar = a = this.foo = a = 3");
    // recursively optimize assigns until optional chaining on RHS
    test(
        lines("a = 3; this.foo = a; a = 3; this.bar = 3; a = 3; this.baz = this?.foo;"),
        lines("a = this.bar = a = this.foo = a = 3; this.baz = this?.foo;"));

    test(
        "a = 4; this.foo = a; a = 3; this.bar = 3; " + "a = 3; this.baz = this.foo;",
        "this.foo = a = 4; a = this.bar = a = 3; this.baz = this.foo");
    // recursively optimize assigns until optional chaining on RHS
    test(
        lines("a = 4; this.foo = a; a = 3; this.bar = 3; a = 3; this.baz = this?.foo;"),
        lines("this.foo = a = 4;", "a = this.bar = a = 3;", "this.baz = this?.foo;"));

    test(
        "a = 3; this.foo = a; a = 4; this.bar = 3; " + "a = 3; this.baz = this.foo;",
        "this.foo = a = 3; a = 4; a = this.bar = 3; this.baz = this.foo");
    test(
        lines("a = 3; this.foo = a; a = 4; this.bar = 3; ", "a = 3; this.baz = this?.foo;"),
        lines("this.foo = a = 3;", "a = 4;", "a = this.bar = 3;", "this.baz = this?.foo;"));
    test(
        "a = 3; this.foo = a; a = 3; this.bar = 3; " + "a = 4; this.baz = this.foo;",
        "this.bar = a = this.foo = a = 3; a = 4; this.baz = this.foo");
    test(
        lines("a = 3; this.foo = a; a = 3; this.bar = 3; a = 4; this.baz = this?.foo;"),
        lines("this.bar = a = this.foo = a = 3;", "a = 4;", "this.baz = this?.foo;"));
  }

  @Test
  public void testBug1840071() {
    // Some external properties are implemented as setters. Let's
    // make sure that we don't collapse them inappropriately.
    test("a.b = a.x; if (a.x) {}", "if (a.b = a.x) {}");
    testSame("a.b = a?.x; if (a?.x) {}");
    testSame("a.b = a.x; if (a.b) {}");
    test("a.b = a.c = a.x; if (a.x) {}", "if (a.b = a.c = a.x) {}");
    testSame("a.b = a.c = a?.x; if (a?.x) {}");

    testSame("a.b = a.c = a.x; if (a.c) {}");
    testSame("a.b = a.c = a.x; if (a.b) {}");
  }

  @Test
  public void testBug2072343() {
    testSame("a = a.x;a = a.x");
    testSame("a = a.x;b = a.x");
    test("b = a.x;a = a.x", "a = b = a.x");
    testSame("b = a?.x;a = a?.x");
    testSame("a.x = a;a = a.x");
    testSame("a.b = a.b.x;a.b = a.b.x");
    testSame("a.y = a.y.x;b = a.y;c = a.y.x");
    test("a = a.x;b = a;c = a.x", "b = a = a.x;c = a.x");
    test("b = a.x;a = b;c = a.x", "a = b = a.x;c = a.x");
  }

  @Test
  public void testBadCollapseIntoCall() {
    // Can't collapse this, because if we did, 'foo' would be called
    // in the wrong 'this' context.
    testSame("this.foo = function() {}; this.foo();");
  }

  @Test
  public void testBadCollapse() {
    testSame("this.$e$ = []; this.$b$ = null;");
  }

  @Test
  public void testIssue1017() {
    testSame("x = x.parentNode.parentNode; x = x.parentNode.parentNode;");
  }

  @Test
  public void testDestructuringLhs_array_idealBehaviours() {
    testSame("a => { ([a] = a); return a; }"); // `a` is being reassigned.
    testSame("a => { ([b] = a); return a; }"); // Evaluating `b` could side-effect `a`.
    testSame("a => { ([a = foo()] = a); return a; }"); // `foo` may be invoked.
    testSame("(a, b) => { (a = [a] = b); return b; }"); // Evaluating `a` could side-effect `b`.
  }

  @Test
  public void testDestructuringLhs_array_backoffBehaviours() {
    // TODO(b/123102446): We really like to collapse some of these chained assignments.

    testSame("(a, b) => { ([a] = a = b); return b; }"); // The middle `a` is redundant.
    testSame("(a, b) => { ([a] = a = b); return a; }"); // The middle `a` is redundant.
    test(
        "(a, b) => { (a = [a] = b); return a; }", // The final `a` is redundant.
        "(a, b) => { return (a = [a] = b); }");
  }

  @Test
  public void testDestructuringLhs_object_idealBehaviours() {
    testSame("a => { ({a} = a); return a; }"); // `a` is being reassigned.
    testSame("a => { ({b} = a); return a; }"); // Evaluating `b` could side-effect `a`.
    testSame("a => { ({a = foo()} = a); return a; }"); // `foo` may be invoked.
    testSame("(a, b) => { (a = {a} = b); return b; }"); // Evaluating `a` could side-effect `b`.
  }

  @Test
  public void testDestructuringLhs_object_backoffBehaviours() {
    // TODO(b/123102446): We really like to collapse some of these chained assignments.

    testSame("(a, b) => { ({a} = a = b); return b; }"); // The middle `a` is redundant.
    testSame("(a, b) => { ({a} = a = b); return a; }"); // The middle `a` is redundant.
    test(
        "(a, b) => { (a = {a} = b); return a; }", // The final `a` is redundant.
        "(a, b) => { return (a = {a} = b); }");
  }

  @Override
  protected CompilerPass getProcessor(Compiler compiler) {
    return new PeepholeOptimizationsPass(compiler, getName(), new ExploitAssigns());
  }
}
