/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.datastax.driver.mapping;

import static org.assertj.core.api.Assertions.assertThat;

import com.datastax.driver.core.CCMTestsSupport;
import com.datastax.driver.core.utils.CassandraVersion;
import com.datastax.driver.mapping.annotations.Column;
import com.datastax.driver.mapping.annotations.Computed;
import com.datastax.driver.mapping.annotations.Field;
import com.datastax.driver.mapping.annotations.Frozen;
import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Table;
import com.datastax.driver.mapping.annotations.Transient;
import com.datastax.driver.mapping.annotations.UDT;
import com.google.common.base.Objects;
import com.google.common.collect.Sets;
import java.util.Set;
import java.util.UUID;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;

/**
 * Tests for JAVA-541 Add polymorphism support to object mapper JAVA-636 Allow @Column annotations
 * on getters/setters as well as fields
 */
@SuppressWarnings({"unused", "WeakerAccess"})
@CassandraVersion("2.1.0")
public class MapperPolymorphismTest extends CCMTestsSupport {

  Circle circle = new Circle(new Point2D(11, 22), 12.34);
  Rectangle rectangle = new Rectangle(new Point2D(20, 30), new Point2D(50, 60));
  Square square = new Square(new Point2D(20, 30), new Point2D(50, 60));
  Sphere sphere = new Sphere(new Point3D(11, 22, 33), 34.56);

  @Override
  public void onTestContextInitialized() {
    execute(
        "CREATE TYPE point2d (\"X\" int, \"Y\" int)",
        "CREATE TYPE point3d (\"X\" int, \"Y\" int, \"Z\" int)",
        "CREATE TABLE circles (circle_id uuid PRIMARY KEY, center2d frozen<point2d>, radius double, tags set<text>)",
        "CREATE TABLE rectangles (rect_id uuid PRIMARY KEY, bottom_left frozen<point2d>, top_right frozen<point2d>, tags set<text>)",
        "CREATE TABLE squares (square_id uuid PRIMARY KEY, bottom_left frozen<point2d>, top_right frozen<point2d>, tags set<text>)",
        "CREATE TABLE spheres (sphere_id uuid PRIMARY KEY, center3d frozen<point3d>, radius double, tags set<text>)");
  }

  @AfterMethod(groups = "short")
  public void clean() {
    execute("TRUNCATE circles", "TRUNCATE rectangles", "TRUNCATE squares", "TRUNCATE spheres");
  }

  @UDT(name = "point2d")
  static class Point2D {

    // test mix of field and getter - getter should win
    @Field(name = "wrong")
    private int x;

    private int y;

    // test private constructor + "immutability"
    private Point2D() {}

    public Point2D(int x, int y) {
      this.x = x;
      this.y = y;
    }

    @Field(name = "X", caseSensitive = true)
    public int getX() {
      return x;
    }

    @Field(name = "Y", caseSensitive = true)
    public int getY() {
      return y;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      Point2D point2D = (Point2D) o;
      return getX() == point2D.getX() && getY() == point2D.getY();
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(getX(), getY());
    }
  }

  @UDT(name = "point3d")
  static class Point3D extends Point2D {

    private int z;

    // test private constructor + "immutability"
    private Point3D() {}

    public Point3D(int x, int y, int z) {
      super(x, y);
      this.z = z;
    }

    @Field(name = "Z", caseSensitive = true)
    public int getZ() {
      return z;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      if (!super.equals(o)) return false;
      Point3D point3D = (Point3D) o;
      return getZ() == point3D.getZ();
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(super.hashCode(), getZ());
    }
  }

  interface Shape2D {

    Set<String> getTags();

    // test annotation on interface method, should get inherited everywhere
    @Transient
    double getArea();
  }

  interface Shape3D {

    double getVolume();
  }

  abstract static class Shape implements Shape2D {

    @PartitionKey // annotated field on superclass; annotation will get inherited in all subclasses
    protected UUID id;

    protected Set<String> tags;

    public Shape() {
      this.id = UUID.randomUUID();
      this.tags = Sets.newHashSet("cool", "awesome");
    }

    @Column(name = "wrong") // gets overridden in all subclasses
    public abstract UUID getId();

    public void setId(UUID id) {
      this.id = id;
    }

    @Override
    public Set<String> getTags() {
      return tags;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      Shape shape = (Shape) o;
      return Objects.equal(getId(), shape.getId());
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(getId());
    }
  }

  @Table(name = "circles")
  static class Circle extends Shape {

    @Column(name = "center2d") // overridden by a getter in Sphere
    @Frozen
    protected Point2D center;

    // tests unusual field name (i.e. does not correspond to getter/setter)
    // will be considered as a separate "property" with no getter nor setter;
    // thus needs to be annotated with @Transient
    @Transient private double _radius;

    private long writeTime;

    public Circle() {}

    public Circle(Point2D center, double radius) {
      this.center = center;
      this._radius = radius;
    }

    @Column(name = "circle_id")
    @Override
    public UUID getId() {
      return id;
    }

    public Point2D getCenter() {
      return center;
    }

    public void setCenter(Point2D center) {
      this.center = center;
    }

    public double getRadius() {
      return _radius;
    }

    // builder-style setter; because the field isn't named in a standard way,
    // if this setter is not detected the test would fail
    public Circle setRadius(double radius) {
      this._radius = radius;
      return this;
    }

    @Override
    // inherits @Transient
    public double getArea() {
      return Math.PI * (Math.pow(getRadius(), 2));
    }

    // tests computed columns - no setter
    @Computed(value = "writetime(\"radius\")")
    public long getWriteTime() {
      return writeTime;
    }

    // mismatched setter - getter is declared in Shape
    public void setTags(Set<String> tags) {
      this.tags = tags;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      if (!super.equals(o)) return false;
      Circle circle = (Circle) o;
      return Double.compare(circle.getRadius(), getRadius()) == 0
          && Objects.equal(getCenter(), circle.getCenter());
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(super.hashCode(), getCenter(), getRadius());
    }
  }

  @Table(name = "rectangles")
  static class Rectangle extends Shape {

    private Point2D bottomLeft;

    private Point2D topRight;

    public Rectangle() {}

    public Rectangle(Point2D bottomLeft, Point2D topRight) {
      this.bottomLeft = bottomLeft;
      this.topRight = topRight;
    }

    @Column(name = "rect_id")
    @Override
    public UUID getId() {
      return id;
    }

    @Column(name = "bottom_left")
    @Frozen
    public Point2D getBottomLeft() {
      return bottomLeft;
    }

    public void setBottomLeft(Point2D bottomLeft) {
      this.bottomLeft = bottomLeft;
    }

    @Column(name = "top_right")
    @Frozen
    public Point2D getTopRight() {
      return topRight;
    }

    public void setTopRight(Point2D topRight) {
      this.topRight = topRight;
    }

    // test annotation in class method
    @Transient
    public double getWidth() {
      return Math.abs(topRight.getX() - bottomLeft.getX());
    }

    @Transient
    public double getHeight() {
      return Math.abs(topRight.getY() - bottomLeft.getY());
    }

    @Override
    // inherits @Transient
    public double getArea() {
      return getWidth() * getHeight();
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      if (!super.equals(o)) return false;
      Rectangle rectangle = (Rectangle) o;
      return Objects.equal(getBottomLeft(), rectangle.getBottomLeft())
          && Objects.equal(getTopRight(), rectangle.getTopRight());
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(super.hashCode(), getBottomLeft(), getTopRight());
    }
  }

  @Table(name = "squares")
  static class Square extends Rectangle {

    public Square() {}

    public Square(Point2D bottomLeft, Point2D topRight) {
      super(bottomLeft, topRight);
      assert getHeight() == getWidth();
    }

    @Column(name = "square_id")
    @Override
    public UUID getId() {
      return id;
    }

    @Override
    // inherits @Column and @Frozen
    public Point2D getBottomLeft() {
      return super.getBottomLeft();
    }

    @Override
    // inherits @Transient
    public double getHeight() {
      return getWidth();
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      if (!super.equals(o)) return false;
      Square square = (Square) o;
      return Objects.equal(getBottomLeft(), square.getBottomLeft())
          && Objects.equal(getTopRight(), square.getTopRight());
    }
  }

  @Table(name = "spheres")
  static class Sphere extends Circle implements Shape3D {

    private long writeTime;

    public Sphere() {}

    public Sphere(Point3D center, double radius) {
      this.center = center;
    }

    @Column(name = "sphere_id")
    @Override
    public UUID getId() {
      return id;
    }

    // overrides field annotation in Circle,
    // note that the property type is narrowed down to Point3D
    @Column(name = "center3d")
    @Frozen
    @Override
    public Point3D getCenter() {
      return (Point3D) center;
    }

    @Override
    public void setCenter(Point2D center) {
      assert center instanceof Point3D;
      this.center = center;
    }

    // overridden builder-style setter
    @Override
    public Sphere setRadius(double radius) {
      super.setRadius(radius);
      return this;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      if (!super.equals(o)) return false;
      Sphere sphere = (Sphere) o;
      return Objects.equal(getCenter(), sphere.getCenter());
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(super.hashCode(), getCenter());
    }

    // test annotation on implementation
    @Override
    @Transient
    public double getVolume() {
      return 4d / 3d * Math.PI * Math.pow(getRadius(), 3);
    }
  }

  /**
   * Validates that fields and methods from a parent class {@link Shape} are inherited for a mapped
   * entity class {@link Circle}.
   *
   * <p>It also ensures that methods overridden in {@link Circle} are given precedence and that
   * their annotations are used. In the case of circle, the {@link Circle#getId()} method overrides
   * the parent and specifies a {@link Column} annotation that will be used instead of the parents
   * annotation ('wrong').
   *
   * <p>Lastly, it defines another column 'radius' that is defined by having {@link
   * Circle#getRadius} and {@link Circle#setRadius} properties, while it has a {@link Transient}
   * field '_radius' that should not be pulled in by the mapper.
   *
   * @test_category object_mapper
   * @jira_ticket JAVA-541, JAVA-636
   */
  @Test(groups = "short")
  public void should_save_and_retrieve_circle() throws Exception {
    MappingManager mappingManager = new MappingManager(session());
    Mapper<Circle> circleMapper = mappingManager.mapper(Circle.class);
    circleMapper.save(circle);
    assertThat(circleMapper.get(circle.getId())).isEqualTo(circle);
  }

  /**
   * Validates that property methods such as {@link Rectangle#getWidth} and {@link
   * Rectangle#getHeight} are not considered as they are marked as {@link Transient}. It also
   * ensures that overridden method {@link Rectangle#getArea} inherits its parent's {@link
   * Transient} annotation.
   *
   * @test_category object_mapper
   * @jira_ticket JAVA-541, JAVA-636
   */
  @Test(groups = "short")
  public void should_save_and_retrieve_rectangle() throws Exception {
    MappingManager mappingManager = new MappingManager(session());
    Mapper<Rectangle> rectangleMapper = mappingManager.mapper(Rectangle.class);
    rectangleMapper.save(rectangle);
    assertThat(rectangleMapper.get(rectangle.getId())).isEqualTo(rectangle);
  }

  /**
   * Validates that {@link Square} inherits its properties from its parent class {@link Rectangle}
   * and its parent's class {@link Shape} and that its methods {@link Square#getBottomLeft} and
   * {@link Square#getHeight} inherit annotations from its parent.
   *
   * @test_category object_mapper
   * @jira_ticket JAVA-541, JAVA-636
   */
  @Test(groups = "short")
  public void should_save_and_retrieve_square() throws Exception {
    MappingManager mappingManager = new MappingManager(session());
    Mapper<Square> squareMapper = mappingManager.mapper(Square.class);
    squareMapper.save(square);
    assertThat(squareMapper.get(square.getId())).isEqualTo(square);
  }

  /**
   * Validates that {@link Sphere} inherits its properties from its parent class {@link Circle} and
   * most importantly can specialize the type of {@link Sphere#center} to a {@link Point3D} instead
   * of a {@link Point2D}.
   *
   * @test_category object_mapper
   * @jira_ticket JAVA-541, JAVA-636
   */
  @Test(groups = "short")
  public void should_save_and_retrieve_sphere() throws Exception {
    MappingManager mappingManager = new MappingManager(session());
    Mapper<Sphere> sphereMapper = mappingManager.mapper(Sphere.class);
    sphereMapper.save(sphere);
    assertThat(sphereMapper.get(sphere.getId())).isEqualTo(sphere);
  }
}
