/*
 * 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.client;

import static java.time.temporal.ChronoField.NANO_OF_SECOND;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasToString;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.math.BigDecimal;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.apache.ignite.lang.IgniteException;
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.apache.ignite.table.mapper.Mapper;
import org.junit.jupiter.api.Test;

/**
 * Record view tests.
 */
public class ClientRecordViewTest extends AbstractClientTableTest {
    @Test
    public void testBinaryPutPojoGet() {
        Table table = defaultTable();
        RecordView<PersonPojo> pojoView = table.recordView(Mapper.of(PersonPojo.class));

        table.recordView().upsert(null, tuple());

        var key = new PersonPojo();
        key.id = DEFAULT_ID;

        PersonPojo val = pojoView.get(null, key);
        PersonPojo missingVal = pojoView.get(null, new PersonPojo());

        assertEquals(DEFAULT_NAME, val.name);
        assertEquals(DEFAULT_ID, val.id);
        assertNull(missingVal);
    }

    @Test
    public void testBinaryPutPrimitiveGet() {
        Table table = defaultTable();
        RecordView<Long> primitiveView = table.recordView(Mapper.of(Long.class));

        table.recordView().upsert(null, tuple());

        Long val = primitiveView.get(null, DEFAULT_ID);
        Long missingVal = primitiveView.get(null, -1L);

        assertEquals(DEFAULT_ID, val);
        assertNull(missingVal);
    }

    @Test
    public void testPrimitivePutBinaryGet() {
        Table table = oneColumnTable();
        RecordView<String> primitiveView = table.recordView(Mapper.of(String.class));

        primitiveView.upsert(null, "abc");

        Tuple tuple = table.recordView().get(null, oneColumnTableKey("abc"));
        assertEquals("abc", tuple.stringValue(0));
    }

    @Test
    public void testMissingValueColumnsThrowException() {
        Table table = fullTable();
        KeyValueView<Tuple, Tuple> kvView = table.keyValueView();
        RecordView<IncompletePojo> pojoView = table.recordView(IncompletePojo.class);

        kvView.put(null, allColumnsTableKey(1), allColumnsTableVal("x", true));

        var key = new IncompletePojo();
        key.id = "1";
        key.gid = 1;

        // This POJO does not have fields for all table columns, which is not allowed (to avoid unexpected data loss).
        IgniteException ex = assertThrows(IgniteException.class, () -> pojoView.get(null, key));
        assertEquals("Failed to deserialize server response: No mapped object field found for column 'ZBOOLEAN'", ex.getMessage());
        assertThat(Arrays.asList(ex.getStackTrace()), anyOf(hasToString(containsString("ClientRecordView"))));
    }

    @Test
    public void testAllColumnsBinaryPutPojoGet() {
        Table table = fullTable();
        RecordView<AllColumnsPojo> pojoView = table.recordView(Mapper.of(AllColumnsPojo.class));

        table.recordView().upsert(null, allColumnsTableVal("foo", false));

        var key = new AllColumnsPojo();
        key.gid = DEFAULT_ID;
        key.id = String.valueOf(DEFAULT_ID);

        AllColumnsPojo res = pojoView.get(null, key);
        assertEquals(11, res.zbyte);
        assertEquals(12, res.zshort);
        assertEquals(13, res.zint);
        assertEquals(14, res.zlong);
        assertEquals(1.5f, res.zfloat);
        assertEquals(1.6, res.zdouble);
        assertEquals(localDate, res.zdate);
        assertEquals(localTime.truncatedTo(ChronoUnit.SECONDS), res.ztime);
        assertEquals(instant.with(NANO_OF_SECOND, truncateNanosToMicros(instant.getNano())), res.ztimestamp);
        assertEquals("foo", res.zstring);
        assertArrayEquals(new byte[]{1, 2}, res.zbytes);
        assertEquals(21, res.zdecimal.longValue());
        assertEquals(uuid, res.zuuid);
    }

    private int truncateNanosToMicros(int nanos) {
        return nanos / 1000 * 1000;
    }

    @Test
    public void testAllColumnsPojoPutBinaryGet() {
        Table table = fullTable();
        RecordView<AllColumnsPojo> pojoView = table.recordView(Mapper.of(AllColumnsPojo.class));

        var val = new AllColumnsPojo();

        val.gid = 111;
        val.id = "112";
        val.zbyte = 113;
        val.zshort = 114;
        val.zint = 115;
        val.zlong = 116;
        val.zfloat = 1.17f;
        val.zdouble = 1.18;
        val.zdate = localDate;
        val.ztime = localTime;
        val.ztimestamp = instant;
        val.zstring = "119";
        val.zbytes = new byte[]{120};
        val.zdecimal = BigDecimal.valueOf(122);
        val.zuuid = uuid;

        pojoView.upsert(null, val);

        Tuple res = table.recordView().get(null, Tuple.create().set("id", "112").set("gid", 111L));

        assertNotNull(res);
        assertEquals(111, res.longValue("gid"));
        assertEquals("112", res.stringValue("id"));
        assertEquals(113, res.byteValue("zbyte"));
        assertEquals(114, res.shortValue("zshort"));
        assertEquals(115, res.intValue("zint"));
        assertEquals(116, res.longValue("zlong"));
        assertEquals(1.17f, res.floatValue("zfloat"));
        assertEquals(1.18, res.doubleValue("zdouble"));
        assertEquals(localDate, res.dateValue("zdate"));
        assertEquals(localTime.truncatedTo(ChronoUnit.SECONDS), res.timeValue("ztime"));
        assertEquals(instant.with(NANO_OF_SECOND, truncateNanosToMicros(instant.getNano())), res.timestampValue("ztimestamp"));
        assertEquals("119", res.stringValue("zstring"));
        assertEquals(120, ((byte[]) res.value("zbytes"))[0]);
        assertDecimalEqual(BigDecimal.valueOf(122), res.decimalValue("zdecimal"));
        assertEquals(uuid, res.uuidValue("zuuid"));
    }

    @Test
    public void testMissingKeyColumnThrowsException() {
        RecordView<NamePojo> recordView = defaultTable().recordView(NamePojo.class);

        IgniteException e = assertThrows(IgniteException.class, () -> recordView.get(null, new NamePojo()));

        assertThat(e.getMessage(), containsString("No mapped object field found for column 'ID'"));
        assertThat(Arrays.asList(e.getStackTrace()), anyOf(hasToString(containsString("ClientRecordView"))));
    }

    @Test
    public void testNullablePrimitiveFields() {
        RecordView<AllColumnsPojoNullable> pojoView = fullTable().recordView(AllColumnsPojoNullable.class);
        RecordView<Tuple> tupleView = fullTable().recordView();

        var rec = new AllColumnsPojoNullable();
        rec.id = "1";
        rec.gid = 1L;

        pojoView.upsert(null, rec);

        AllColumnsPojoNullable res = pojoView.get(null, rec);
        Tuple binRes = tupleView.get(null, Tuple.create().set("id", "1").set("gid", 1L));

        assertNotNull(res);
        assertNotNull(binRes);

        assertNull(res.zboolean);
        assertNull(res.zbyte);
        assertNull(res.zshort);
        assertNull(res.zint);
        assertNull(res.zlong);
        assertNull(res.zfloat);
        assertNull(res.zdouble);

        for (int i = 0; i < binRes.columnCount(); i++) {
            if (binRes.columnName(i).endsWith("ID")) {
                continue;
            }

            assertNull(binRes.value(i));
        }
    }

    @Test
    public void testGetAll() {
        Table table = defaultTable();
        RecordView<PersonPojo> pojoView = table.recordView(Mapper.of(PersonPojo.class));

        table.recordView().upsert(null, tuple());
        table.recordView().upsert(null, tuple(100L, "100"));

        Collection<PersonPojo> keys = List.of(
                new PersonPojo(DEFAULT_ID, "blabla"),
                new PersonPojo(101L, "1234"),
                new PersonPojo(100L, "qwerty"));

        PersonPojo[] res = pojoView.getAll(null, keys).toArray(new PersonPojo[0]);

        assertEquals(3, res.length);

        assertNotNull(res[0]);
        assertNull(res[1]);
        assertNotNull(res[2]);

        assertEquals(DEFAULT_ID, res[0].id);
        assertEquals(DEFAULT_NAME, res[0].name);

        assertEquals(100L, res[2].id);
        assertEquals("100", res[2].name);
    }

    @Test
    public void testGetAllPrimitive() {
        Table table = oneColumnTable();
        RecordView<String> primitiveView = table.recordView(Mapper.of(String.class));

        primitiveView.upsertAll(null, List.of("a", "c"));

        String[] res = primitiveView.getAll(null, List.of("a", "b", "c")).toArray(new String[0]);

        assertEquals("a", res[0]);
        assertNull(res[1]);
        assertEquals("c", res[2]);
    }

    @Test
    public void testContains() {
        RecordView<PersonPojo> recordView = defaultTable().recordView(PersonPojo.class);
        PersonPojo pojo = new PersonPojo(DEFAULT_ID, DEFAULT_NAME);

        recordView.insert(null, pojo);

        assertTrue(recordView.contains(null, pojo));
        assertTrue(recordView.contains(null, new PersonPojo(DEFAULT_ID, "")));
        assertFalse(recordView.contains(null, new PersonPojo(DEFAULT_ID - 1, DEFAULT_NAME)));
    }

    @Test
    public void testContainsAll() {
        RecordView<PersonPojo> recordView = defaultTable().recordView(PersonPojo.class);

        long firstKey = 101L;
        PersonPojo firstPerson = new PersonPojo(firstKey, "201");

        long secondKey = 102L;
        PersonPojo secondPerson = new PersonPojo(secondKey, "202");

        long thirdKey = 103L;
        PersonPojo thirdPerson = new PersonPojo(thirdKey, "203");

        List<PersonPojo> recs = List.of(firstPerson, secondPerson, thirdPerson);

        recordView.insertAll(null, recs);

        assertThrows(NullPointerException.class, () -> recordView.containsAll(null, null));
        assertThrows(NullPointerException.class, () -> recordView.containsAll(null, List.of(firstPerson, null, thirdPerson)));

        assertTrue(recordView.containsAll(null, List.of()));
        assertTrue(recordView.containsAll(null, List.of(firstPerson)));
        assertTrue(recordView.containsAll(null, List.of(firstPerson, secondPerson, thirdPerson)));
        assertTrue(recordView.containsAll(null, List.of(
                new PersonPojo(firstKey, " "),
                new PersonPojo(secondKey, " "),
                new PersonPojo(thirdKey, " ")
        )));

        long zeroKey = 0L;
        PersonPojo missedPojo = new PersonPojo(zeroKey, DEFAULT_NAME);

        assertFalse(recordView.containsAll(null, List.of(missedPojo)));
        assertFalse(recordView.containsAll(null, List.of(firstPerson, secondPerson, missedPojo)));
        assertFalse(recordView.containsAll(null, List.of(
                new PersonPojo(firstKey, " "),
                new PersonPojo(secondKey, " "),
                new PersonPojo(zeroKey, " ")
        )));
    }    

    @Test
    public void testUpsertAll() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        Collection<PersonPojo> pojos = List.of(
                new PersonPojo(DEFAULT_ID, DEFAULT_NAME),
                new PersonPojo(100L, "100"),
                new PersonPojo(101L, "101"));

        pojoView.upsertAll(null, pojos);

        assertEquals(DEFAULT_NAME, pojoView.get(null, new PersonPojo(DEFAULT_ID)).name);
        assertEquals("100", pojoView.get(null, new PersonPojo(100L)).name);
        assertEquals("101", pojoView.get(null, new PersonPojo(101L)).name);
    }

    @Test
    public void testGetAndUpsert() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        pojoView.upsert(null, new PersonPojo(DEFAULT_ID, DEFAULT_NAME));

        PersonPojo res1 = pojoView.getAndUpsert(null, new PersonPojo(DEFAULT_ID, "new_name"));
        PersonPojo res2 = pojoView.getAndUpsert(null, new PersonPojo(100L, "name"));

        assertEquals(DEFAULT_NAME, res1.name);
        assertEquals("new_name", pojoView.get(null, new PersonPojo(DEFAULT_ID)).name);

        assertNull(res2);
        assertEquals("name", pojoView.get(null, new PersonPojo(100L)).name);
    }

    @Test
    public void testInsert() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        pojoView.upsert(null, new PersonPojo(DEFAULT_ID, DEFAULT_NAME));

        boolean res1 = pojoView.insert(null, new PersonPojo(DEFAULT_ID, "foobar"));
        boolean res2 = pojoView.insert(null, new PersonPojo(100L, "100"));

        assertFalse(res1);
        assertTrue(res2);
        assertEquals("100", pojoView.get(null, new PersonPojo(100L)).name);
    }

    @Test
    public void testInsertAll() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        pojoView.upsert(null, new PersonPojo(DEFAULT_ID, DEFAULT_NAME));

        Collection<PersonPojo> res1 = pojoView.insertAll(null, List.of(new PersonPojo(10L, "10"), new PersonPojo(20L)));
        Collection<PersonPojo> res2 = pojoView.insertAll(null, List.of(new PersonPojo(DEFAULT_ID), new PersonPojo(10L)));
        Collection<PersonPojo> res3 = pojoView.insertAll(
                null,
                List.of(new PersonPojo(DEFAULT_ID, "new_name"), new PersonPojo(30L))
        );

        assertEquals(0, res1.size());
        assertEquals(2, res2.size());
        assertEquals(1, res3.size());

        assertEquals("10", pojoView.get(null, new PersonPojo(10L)).name);
        assertNull(pojoView.get(null, new PersonPojo(20L)).name);

        assertEquals("new_name", res3.iterator().next().name);
    }

    @Test
    public void testReplace() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        pojoView.upsert(null, new PersonPojo(DEFAULT_ID, DEFAULT_NAME));

        assertFalse(pojoView.replace(null, new PersonPojo(-1L)));
        assertTrue(pojoView.replace(null, new PersonPojo(DEFAULT_ID, "new_name")));

        assertNull(pojoView.get(null, new PersonPojo(-1L)));
        assertEquals("new_name", pojoView.get(null, new PersonPojo(DEFAULT_ID)).name);
    }

    @Test
    public void testReplaceExact() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        pojoView.upsert(null, new PersonPojo(DEFAULT_ID, DEFAULT_NAME));

        assertFalse(pojoView.replace(null, new PersonPojo(DEFAULT_ID, "x"), new PersonPojo(DEFAULT_ID, "new_name")));
        assertFalse(pojoView.replace(null, new PersonPojo(-1L, "x"), new PersonPojo(DEFAULT_ID, "new_name")));
        assertTrue(pojoView.replace(null, new PersonPojo(DEFAULT_ID, DEFAULT_NAME), new PersonPojo(DEFAULT_ID, "new_name2")));

        assertNull(pojoView.get(null, new PersonPojo(-1L)));
        assertEquals("new_name2", pojoView.get(null, new PersonPojo(DEFAULT_ID)).name);
    }

    @Test
    public void testGetAndReplace() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        pojoView.upsert(null, new PersonPojo(DEFAULT_ID, DEFAULT_NAME));

        PersonPojo res1 = pojoView.getAndReplace(null, new PersonPojo(DEFAULT_ID, "new_name"));
        PersonPojo res2 = pojoView.getAndReplace(null, new PersonPojo(100L, "name"));

        assertEquals(DEFAULT_NAME, res1.name);
        assertEquals("new_name", pojoView.get(null, new PersonPojo(DEFAULT_ID)).name);

        assertNull(res2);
        assertNull(pojoView.get(null, new PersonPojo(100L)));
    }

    @Test
    public void testDelete() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        pojoView.upsert(null, new PersonPojo(DEFAULT_ID, DEFAULT_NAME));

        boolean res1 = pojoView.delete(null, new PersonPojo(DEFAULT_ID));
        boolean res2 = pojoView.delete(null, new PersonPojo(100L, "name"));

        assertTrue(res1);
        assertFalse(res2);

        assertNull(pojoView.get(null, new PersonPojo(DEFAULT_ID)));
    }

    @Test
    public void testDeleteExact() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        pojoView.upsert(null, new PersonPojo(DEFAULT_ID, DEFAULT_NAME));
        pojoView.upsert(null, new PersonPojo(100L, "100"));

        boolean res1 = pojoView.deleteExact(null, new PersonPojo(DEFAULT_ID));
        boolean res2 = pojoView.deleteExact(null, new PersonPojo(100L));
        boolean res3 = pojoView.deleteExact(null, new PersonPojo(100L, "100"));

        assertFalse(res1);
        assertFalse(res2);
        assertTrue(res3);

        assertNotNull(pojoView.get(null, new PersonPojo(DEFAULT_ID)));
        assertNull(pojoView.get(null, new PersonPojo(100L)));
    }

    @Test
    public void testGetAndDelete() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        pojoView.upsert(null, new PersonPojo(DEFAULT_ID, DEFAULT_NAME));

        PersonPojo res1 = pojoView.getAndDelete(null, new PersonPojo(DEFAULT_ID));
        PersonPojo res2 = pojoView.getAndDelete(null, new PersonPojo(100L));

        assertEquals(DEFAULT_NAME, res1.name);
        assertNull(pojoView.get(null, new PersonPojo(DEFAULT_ID)));

        assertNull(res2);
    }

    @Test
    public void testDeleteAll() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        pojoView.upsertAll(
                null,
                List.of(new PersonPojo(1L, "1"), new PersonPojo(2L, "2"), new PersonPojo(3L, "3"))
        );

        Collection<PersonPojo> res1 = pojoView.deleteAll(null, List.of(new PersonPojo(10L), new PersonPojo(20L)));
        Collection<PersonPojo> res2 = pojoView.deleteAll(null, List.of(new PersonPojo(1L), new PersonPojo(3L)));

        assertEquals(2, res1.size());
        assertEquals(0, res2.size());

        assertNull(pojoView.get(null, new PersonPojo(1L)));
        assertEquals("2", pojoView.get(null, new PersonPojo(2L)).name);
        assertNull(pojoView.get(null, new PersonPojo(3L)));
    }

    @Test
    public void testDeleteAllExact() {
        RecordView<PersonPojo> pojoView = defaultTable().recordView(Mapper.of(PersonPojo.class));

        pojoView.upsertAll(
                null,
                List.of(new PersonPojo(1L, "1"), new PersonPojo(2L, "2"), new PersonPojo(3L, "3"))
        );

        Collection<PersonPojo> res1 = pojoView.deleteAllExact(
                null,
                List.of(new PersonPojo(1L, "a"), new PersonPojo(3L, "b"))
        );

        Collection<PersonPojo> res2 = pojoView.deleteAllExact(
                null,
                List.of(new PersonPojo(1L, "1"), new PersonPojo(3L, "3"))
        );

        assertEquals(2, res1.size());
        assertEquals(0, res2.size());

        assertNull(pojoView.get(null, new PersonPojo(1L)));
        assertEquals("2", pojoView.get(null, new PersonPojo(2L)).name);
        assertNull(pojoView.get(null, new PersonPojo(3L)));
    }

    @Test
    public void testNullableColumnWithDefaultValueSetNullReturnsNull() {
        Table table = tableWithDefaultValues();
        RecordView<Tuple> recordView = table.recordView();
        RecordView<DefaultValuesPojo> pojoView = table.recordView(DefaultValuesPojo.class);

        var pojo = new DefaultValuesPojo();
        pojo.id = 1;
        pojo.str = null;
        pojo.strNonNull = "s";

        pojoView.upsert(null, pojo);

        var res = recordView.get(null, Tuple.create().set("id", 1));

        assertNull(res.stringValue("str"));
    }

    @Test
    public void testNonNullableColumnWithDefaultValueSetNullThrowsException() {
        Table table = tableWithDefaultValues();
        RecordView<DefaultValuesPojo> pojoView = table.recordView(DefaultValuesPojo.class);

        var pojo = new DefaultValuesPojo();
        pojo.id = 1;
        pojo.strNonNull = null;

        var ex = assertThrows(IgniteException.class, () -> pojoView.upsert(null, pojo));

        assertThat(ex.getMessage(), containsString("Column 'STRNONNULL' does not allow NULLs"));
        assertThat(Arrays.asList(ex.getStackTrace()), anyOf(hasToString(containsString("ClientRecordView"))));
    }
}
