/*
 * 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 org.jdbi.v3.core.mapper;

import java.util.Objects;
import java.util.stream.IntStream;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.junit5.H2DatabaseExtension;
import org.jdbi.v3.core.mapper.reflect.ConstructorMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

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

public class JoinRowMapperTest {

    @RegisterExtension
    public H2DatabaseExtension h2Extension = H2DatabaseExtension.instance();

    private Handle h;

    @BeforeEach
    public void setUp() {
        h = h2Extension.getSharedHandle();
        h.execute("CREATE TABLE \"user\" ("
            + "uid INTEGER NOT NULL,"
            + "name VARCHAR NOT NULL"
            + ")");
        h.execute("CREATE TABLE article ("
            + "aid INTEGER NOT NULL,"
            + "title VARCHAR NOT NULL"
            + ")");
        h.execute("CREATE TABLE author ("
            + "uid INTEGER NOT NULL,"
            + "aid INTEGER NOT NULL"
            + ")");

        IntStream.rangeClosed(1, 3).forEach(u ->
            h.execute("INSERT INTO \"user\" (uid, name) VALUES (?, ?)", u, "u" + u));

        IntStream.rangeClosed(1, 3).forEach(a ->
            h.execute("INSERT INTO article (aid, title) VALUES (?, ?)", a, "a" + a));

        h.prepareBatch("INSERT INTO author (uid, aid) VALUES (?,?)")
            .bind(0, 1).bind(1, 2).add()
            .bind(0, 3).bind(1, 1).add()
            .bind(0, 3).bind(1, 3).add()
            .execute();

        // tag::mapperSetup[]
        h.registerRowMapper(ConstructorMapper.factory(User.class));
        h.registerRowMapper(ConstructorMapper.factory(Article.class));
        // end::mapperSetup[]
    }

    @Test
    public void testCartesianProduct() {
        Multimap<User, Article> product = HashMultimap.create();
        h.createQuery("SELECT * FROM \"user\", article")
            .map(JoinRowMapper.forTypes(User.class, Article.class))
            .forEach(jr -> product.put(jr.get(User.class), jr.get(Article.class)));

        Multimap<User, Article> expected = HashMultimap.create();
        IntStream.rangeClosed(1, 3).forEach(u ->
            IntStream.rangeClosed(1, 3).forEach(a ->
                expected.put(u(u), a(a))));

        assertThat(product).isEqualTo(expected);
    }

    @Test
    public void testJoin() {
       // tag::multimap[]
        Multimap<User, Article> joined = HashMultimap.create();
        h.createQuery("SELECT * FROM \"user\" NATURAL JOIN author NATURAL JOIN article")
            .map(JoinRowMapper.forTypes(User.class, Article.class))
            .forEach(jr -> joined.put(jr.get(User.class), jr.get(Article.class)));
       // end::multimap[]

        assertThat(joined).isEqualTo(getExpected());
    }

    public static Multimap<User, Article> getExpected() {
        Multimap<User, Article> expected = HashMultimap.create();
        expected.put(u(1), a(2));
        expected.put(u(3), a(1));
        expected.put(u(3), a(3));
        return expected;
    }

    private static User u(int uid) {
        return new User(uid, "u" + uid);
    }

    private static Article a(int aid) {
        return new Article(aid, "a" + aid);
    }

    public static class User {
        private final int uid;
        private final String name;

        public User(int uid, String name) {
            this.uid = uid;
            this.name = name;
        }

        @Override
        public int hashCode() {
            return Objects.hash(uid, name);
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof User) {
                User that = (User) obj;
                return Objects.equals(uid, that.uid) && Objects.equals(name, that.name);
            }
            return false;
        }
    }

    public static class Article {
        private final int aid;
        private final String title;

        public Article(int aid, String title) {
            this.aid = aid;
            this.title = title;
        }

        @Override
        public int hashCode() {
            return Objects.hash(aid, title);
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof Article) {
                Article that = (Article) obj;
                return Objects.equals(aid, that.aid) && Objects.equals(title, that.title);
            }
            return false;
        }
    }

}
