/*
 * Copyright DataStax, Inc.
 *
 * 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.datastax.driver.mapping;

import com.datastax.driver.core.CCMTestsSupport;
import com.datastax.driver.core.utils.CassandraVersion;
import com.datastax.driver.mapping.annotations.*;
import com.google.common.base.Objects;
import com.google.common.collect.Sets;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;

import java.util.Set;
import java.util.UUID;

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

/**
 * 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();

    }

    static abstract 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);
    }

}
