/*
 * 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 org.apache.ignite.internal.sql.engine;

import static org.apache.ignite.internal.sql.engine.util.SqlTestUtils.assertThrowsSqlException;
import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrows;
import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCause;
import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.util.AbstractMap.SimpleEntry;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.SubmissionPublisher;
import org.apache.ignite.Ignite;
import org.apache.ignite.internal.ClusterPerClassIntegrationTest;
import org.apache.ignite.lang.ErrorGroups.Sql;
import org.apache.ignite.lang.MarshallerException;
import org.apache.ignite.sql.ResultSet;
import org.apache.ignite.sql.SqlRow;
import org.apache.ignite.table.DataStreamerItem;
import org.apache.ignite.table.KeyValueView;
import org.apache.ignite.table.RecordView;
import org.apache.ignite.table.Table;
import org.apache.ignite.table.Tuple;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

/**
 * NOT NULL constraint tests.
 */
public abstract class ItNotNullConstraintTest extends ClusterPerClassIntegrationTest {

    @AfterEach
    public void clear() {
        dropAllTables();
    }

    protected abstract Ignite ignite();

    protected final Table table(String name) {
        return ignite().tables().table(name);
    }

    protected void runSql(String stmt) {
        Ignite ignite = ignite();
        try (ResultSet<SqlRow> rs = ignite.sql().execute(null, stmt)) {
            assertNotNull(rs);
        }
    }

    @Test
    public void testSqlNotNullConstraints() {
        sql("CREATE TABLE t1 (id INTEGER PRIMARY KEY, int_col INTEGER NOT NULL)");

        // INSERT
        assertThrowsSqlException(
                Sql.CONSTRAINT_VIOLATION_ERR,
                "Column 'ID' does not allow NULLs",
                () -> runSql("INSERT INTO t1 VALUES(NULL, 1)"));

        // KV case
        assertThrowsSqlException(
                Sql.CONSTRAINT_VIOLATION_ERR,
                "Column 'INT_COL' does not allow NULLs",
                () -> runSql("INSERT INTO t1 VALUES(1, NULL)"));

        // General case

        assertThrowsSqlException(
                Sql.CONSTRAINT_VIOLATION_ERR,
                "Column 'INT_COL' does not allow NULLs",
                () -> runSql("INSERT INTO t1 VALUES(1, (SELECT NULL))"));

        assertThrowsSqlException(
                Sql.CONSTRAINT_VIOLATION_ERR,
                "Column 'INT_COL' does not allow NULLs",
                () -> runSql("INSERT INTO t1 SELECT 1, NULL"));

        assertThrowsSqlException(
                Sql.CONSTRAINT_VIOLATION_ERR,
                "Column 'INT_COL' does not allow NULLs",
                () -> runSql("INSERT INTO t1 (id) VALUES(1)"));

        // UPDATE
        sql("INSERT INTO t1 VALUES(1, 42)");

        assertThrowsSqlException(
                Sql.STMT_VALIDATION_ERR,
                "Cannot update field \"ID\". Primary key columns are not modifiable",
                () -> runSql("UPDATE t1 SET id = NULL WHERE val = 42"));

        // KV case
        assertThrowsSqlException(
                Sql.CONSTRAINT_VIOLATION_ERR,
                "Column 'INT_COL' does not allow NULLs",
                () -> runSql("UPDATE t1 SET int_col = null WHERE id = 1"));

        // General case

        assertThrowsSqlException(
                Sql.CONSTRAINT_VIOLATION_ERR,
                "Column 'INT_COL' does not allow NULLs",
                () -> runSql("UPDATE t1 SET int_col = null"));

        // MERGE
        runSql("CREATE TABLE t2 (id INTEGER PRIMARY KEY, int_col INTEGER NOT NULL)");

        runSql("INSERT INTO t2 VALUES (1, 42)");

        assertThrowsSqlException(
                Sql.CONSTRAINT_VIOLATION_ERR,
                "Column 'INT_COL' does not allow NULLs",
                () -> runSql("MERGE INTO t2 dst USING t1 src ON dst.id = src.id WHEN MATCHED THEN UPDATE SET int_col = NULL"));

        runSql("INSERT INTO t1 VALUES (2, 71)");

        assertThrowsSqlException(
                Sql.CONSTRAINT_VIOLATION_ERR,
                "Column 'INT_COL' does not allow NULLs",
                () -> runSql("MERGE INTO t2 dst USING t1 src ON dst.id = src.id "
                        + "WHEN NOT MATCHED THEN INSERT (id, int_col) VALUES (src.id, NULL)"));

    }

    @Test
    public void testKeyValueView() {
        sql("CREATE TABLE kv (id INTEGER PRIMARY KEY, val INTEGER NOT NULL)");

        Table table = table("KV");

        {
            KeyValueView<Integer, Integer> view = table.keyValueView(Integer.class, Integer.class);
            assertThrows(MarshallerException.class, () -> {
                view.put(null, 1, null);
            }, "Column 'VAL' does not allow NULLs");
        }

        {
            KeyValueView<Tuple, Tuple> view = table.keyValueView();
            assertThrows(MarshallerException.class, () -> {
                view.put(null, Tuple.create(Map.of("id", 1)), Tuple.create());
            }, "Column 'VAL' does not allow NULLs");
        }

        {
            KeyValueView<Integer, Val> view = table.keyValueView(Integer.class, Val.class);
            assertThrows(MarshallerException.class, () -> {
                Val val = new Val();
                view.put(null, 1, val);
            }, "Column 'VAL' does not allow NULLs");
        }
    }

    @Test
    public void testRecordView() {
        sql("CREATE TABLE kv (id INTEGER PRIMARY KEY, val INTEGER NOT NULL)");

        Table table = table("KV");

        {
            RecordView<Rec> view = table.recordView(Rec.class);

            assertThrows(MarshallerException.class, () -> {
                Rec rec = new Rec();
                view.insert(null, rec);
            }, "Column 'ID' does not allow NULLs");
        }

        {
            RecordView<Rec> view = table.recordView(Rec.class);
            assertThrows(MarshallerException.class, () -> {
                Rec rec = new Rec();
                rec.id = 42;
                view.insert(null, rec);
            }, "Column 'VAL' does not allow NULLs");
        }

        {
            RecordView<Tuple> view = table.recordView();
            assertThrows(MarshallerException.class, () -> {
                view.insert(null, Tuple.create(Map.of("id", 1)));
            }, "Column 'VAL' does not allow NULLs");
        }
    }

    @Test
    public void testKeyValueViewDataStreamer() {
        sql("CREATE TABLE kv (id INTEGER PRIMARY KEY, val INTEGER NOT NULL)");

        Table table = table("KV");

        {
            KeyValueView<Tuple, Tuple> view = table.keyValueView();

            checkDataStreamer(view, new SimpleEntry<>(Tuple.create(Map.of("id", 1)), Tuple.create()), "Column 'VAL' does not allow NULLs");
        }

        {
            KeyValueView<Integer, Integer> view = table.keyValueView(Integer.class, Integer.class);
            checkDataStreamer(view, new SimpleEntry<>(1, null), "Column 'VAL' does not allow NULLs");
        }

        {
            KeyValueView<Integer, Val> view = table.keyValueView(Integer.class, Val.class);
            Val val = new Val();
            checkDataStreamer(view, new SimpleEntry<>(1, val), "Column 'VAL' does not allow NULLs");
        }
    }

    @Test
    public void testRecordViewDataStreamer() {
        sql("CREATE TABLE kv (id INTEGER PRIMARY KEY, val INTEGER NOT NULL)");

        Table table = table("KV");

        {
            RecordView<Tuple> view = table.recordView();
            checkDataStreamer(view, Tuple.create(Map.of("id", 1)), "Column 'VAL' does not allow NULLs");
        }

        {
            RecordView<Rec> view = table.recordView(Rec.class);
            Rec rec = new Rec();
            checkDataStreamer(view, rec, "Column 'ID' does not allow NULLs");
        }

        {
            RecordView<Rec> view = table.recordView(Rec.class);
            Rec rec = new Rec();
            rec.id = 1;
            checkDataStreamer(view, rec, "Column 'VAL' does not allow NULLs");
        }
    }

    private static <K, V> void checkDataStreamer(KeyValueView<K, V> view, Entry<K, V> item, String error) {
        CompletableFuture<Void> streamerFut;

        try (var publisher = new SubmissionPublisher<DataStreamerItem<Entry<K, V>>>()) {
            streamerFut = view.streamData(publisher, null);
            publisher.submit(DataStreamerItem.of(item));
        }

        assertThrowsWithCause(() -> await(streamerFut), MarshallerException.class, error);
    }

    private static <R> void checkDataStreamer(RecordView<R> view, R item, String error) {
        CompletableFuture<Void> streamerFut;

        try (var publisher = new SubmissionPublisher<DataStreamerItem<R>>()) {
            streamerFut = view.streamData(publisher, null);
            publisher.submit(DataStreamerItem.of(item));
        }

        assertThrowsWithCause(() -> await(streamerFut), MarshallerException.class, error);
    }

    static class Val {
        @SuppressWarnings("unused")
        Integer val;
    }

    static class Rec {
        Integer id;
        @SuppressWarnings("unused")
        Integer val;
    }
}
