import org.scalacheck._, Prop._, Gen._, Arbitrary._
import scala.reflect.runtime.universe._, Flag._

object TermDeconstructionProps extends QuasiquoteProperties("term deconstruction") {
  property("f(..x) = f") = test {
    // see SI-8008
    assertThrows[MatchError] {
      val q"f(..$args)" = q"f"
    }
  }

  property("f(x)") = forAll { (x: Tree) =>
    val q"f($x1)" = q"f($x)"
    x1 ≈ x
  }

  property("f(..xs)") = forAll { (x1: Tree, x2: Tree) =>
    val q"f(..$xs)" = q"f($x1, $x2)"
    xs ≈ List(x1, x2)
  }

  property("f(y, ..ys)") = forAll { (x1: Tree, x2: Tree, x3: Tree) =>
    val q"f($y, ..$ys)" = q"f($x1, $x2, $x3)"
    y ≈ x1 && ys ≈ List(x2, x3)
  }

  property("f(y1, y2, ..ys)") = forAll { (x1: Tree, x2: Tree, x3: Tree) =>
    val q"f($y1, $y2, ..$ys)" = q"f($x1, $x2, $x3)"
    y1 ≈ x1 && y2 ≈ x2 && ys ≈ List(x3)
  }

  property("f(y1, ..ys, yn)") = forAll { (x1: Tree, x2: Tree, x3: Tree, x4: Tree) =>
    val q"f($y1, ..$ys, $yn)" = q"f($x1, $x2, $x3, $x4)"
    y1 ≈ x1 && ys ≈ List(x2, x3) && yn ≈ x4
  }

  property("f(..ys, y_{n-1}, y_n)") = forAll { (x1: Tree, x2: Tree, x3: Tree, x4: Tree) =>
    val q"f(..$ys, $yn1, $yn)" = q"f($x1, $x2, $x3, $x4)"
    ys ≈ List(x1, x2) && yn1 ≈ x3 && yn ≈ x4
  }

  property("f(...xss)") = forAll { (x1: Tree, x2: Tree) =>
    val q"f(...$xss)" = q"f($x1)($x2)"
    xss ≈ List(List(x1), List(x2))
  }

  property("f(...$xss)(..$last)") = forAll { (x1: Tree, x2: Tree, x3: Tree) =>
    val q"f(...$xss)(..$last)" = q"f($x1)($x2)($x3)"
    xss ≈ List(List(x1), List(x2)) && last ≈ List(x3)
  }

  property("f(...$xss)(..$lastinit, $lastlast)") = forAll { (x1: Tree, x2: Tree, x3: Tree, x4: Tree) =>
    val q"f(...$xss)(..$lastinit, $lastlast)" = q"f($x1)($x2, $x3, $x4)"
    xss ≈ List(List(x1)) && lastinit ≈ List(x2, x3) && lastlast ≈ x4
  }

  property("f(...xss) = f") = forAll { (x1: Tree, x2: Tree) =>
    val q"f(...$xss)" = q"f"
    xss ≈ List()
  }

  property("deconstruct unit as tuple") = test {
    val q"(..$xs)" = q"()"
    assert(xs.isEmpty)
  }

  property("deconstruct tuple") = test {
    val q"(..$xs)" = q"(a, b)"
    assert(xs ≈ List(q"a", q"b"))
  }

  property("deconstruct tuple mixed") = test {
    val q"($first, ..$rest)" = q"(a, b, c)"
    assert(first ≈ q"a")
    assert(rest ≈ List(q"b", q"c"))
  }

  property("deconstruct tuple last element") = test {
    val q"($first, ..$rest, $last)" = q"(a, b, c, d)"
    assert(first ≈ q"a")
    assert(rest ≈ List(q"b", q"c"))
    assert(last ≈ q"d")
  }

  property("deconstruct expr as tuple") = test {
    val q"(..$elems)" = q"foo"
    assert(elems ≈ List(q"foo"))
  }

  property("deconstruct cases") = test {
    val q"$x match { case ..$cases }" = q"x match { case 1 => case 2 => }"
    assert(x ≈ q"x")
    assert(cases ≈ List(cq"1 =>", cq"2 =>"))
  }

  property("deconstruct splitting last case") = test {
    val q"$_ match { case ..$cases case $last }" = q"x match { case 1 => case 2 => case 3 => }"
    assert(cases ≈ List(cq"1 =>", cq"2 =>"))
    assert(last ≈ cq"3 =>")
  }

  property("deconstruct block") = test {
    val q"{ ..$xs }" = q"{ x1; x2; x3 }"
    assert(xs ≈ List(q"x1", q"x2", q"x3"))
  }

  property("deconstruct last element of a block") = test {
    val q"{ ..$xs; $x }" = q"x1; x2; x3; x4"
    assert(xs ≈ List(q"x1", q"x2", q"x3"))
    assert(x ≈ q"x4")
  }

  property("exhaustive function matcher") = test {
    def matches(line: String) { val q"(..$args) => $body" = parse(line) }
    matches("() => bippy")
    matches("(y: Y) => y oh y")
    matches("(x: X, y: Y) => x and y")
  }

  property("exhaustive new pattern") = test {
    def matches(line: String) {
      val q"new { ..$early } with $name[..$targs](...$vargss) with ..$mixin { $self => ..$body }" = parse(line)
    }
    matches("new foo")
    matches("new foo { body }")
    matches("new foo[t]")
    matches("new foo(x)")
    matches("new foo[t](x)")
    matches("new foo[t](x) { body }")
    matches("new foo with bar")
    matches("new foo with bar { body }")
    matches("new { anonymous }")
    matches("new { val early = 1 } with Parent[Int] { body }")
    matches("new Foo { selfie => }")
  }

  property("exhaustive assign pattern") = test {
    def matches(tree: Tree) { val q"$rhs = $lhs" = tree }
    matches(parse("left = right"))
    matches(parse("arr(1) = 2"))
    matches(AssignOrNamedArg(EmptyTree, EmptyTree))
  }

  property("deconstruct update 1") = test {
    val q"$obj(..$args) = $value" = q"foo(bar) = baz"
    assert(obj ≈ q"foo")
    assert(args ≈ List(q"bar"))
    assert(value ≈ q"baz")
  }

  property("deconstruct update 2") = test {
    val q"$left = $value" = q"foo(bar) = baz"
    assert(left ≈ q"foo(bar)")
    assert(value ≈ q"baz")
  }

  property("deconstruct while loop") = test {
    val q"while($cond) $body" = parse("while(cond) body")
    assert(cond ≈ q"cond")
    assert(body ≈ q"body")
  }

  property("deconstruct do while loop") = test {
    val q"do $body while($cond)" = parse("do body while(cond)")
    assert(cond ≈ q"cond")
    assert(body ≈ q"body")
  }

  property("deconstruct anonymous function with placeholders") = test {
    val q"{ $f(_) }" = q"{ foo(_) }"
    assert(f ≈ q"foo")
    val q"{ _.$member }" = q"{ _.foo }"
    assert(member ≈ TermName("foo"))
    val q"{ _ + $x }" = q"{ _ + x }"
    assert(x ≈ q"x")
    val q"{ _ * _ }" = q"{ _ * _ }"
  }

  property("si-8275 a") = test {
    val cq"_ => ..$stats" = cq"_ => foo; bar"
    assert(stats ≈ List(q"foo", q"bar"))
  }

  property("si-8275 b") = test {
    val cq"_ => ..$init; $last" = cq"_ => a; b; c"
    assert(init ≈ List(q"a", q"b"))
    assert(last ≈ q"c")
  }

  property("si-8275 c") = test {
    val cq"_ => ..$stats" = cq"_ =>"
    assert(stats.isEmpty)
    assertEqAst(q"{ case _ => ..$stats }", "{ case _ => }")
  }

  property("can't flatten type into block") = test {
    assertThrows[IllegalArgumentException] {
      val tpt = tq"List[Int]"
      q"..$tpt; ()"
    }
  }

  property("term select doesn't match type select") = test {
    assertThrows[MatchError] {
      val q"$qual.$name" = tq"foo.bar"
    }
  }

  property("type application doesn't match applied type") = test {
    assertThrows[MatchError] {
      val q"$f[..$targs]" = tq"foo[bar]"
    }
  }

  property("match doesn't match partial function") = test {
    assertThrows[MatchError] {
      val q"$_ match { case ..$_ }" = q"{ case _ => }"
    }
  }

  property("deconstruct partial function") = test {
    val q"{ case ..$cases }" = q"{ case a => b case c => d }"
    val List(cq"a => b", cq"c => d") = cases
  }
}
