/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.products.ov.commons.ui.ws.rs;

import static java.nio.file.Files.createTempFile;
import static java.nio.file.Files.delete;
import static java.nio.file.Files.write;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;

/**
 * Tests of {@link ObjectMappers}.
 */
public class ObjectMappersTest {
    private static final String CSV = "A,1";

    private Foo expected;
    
    @Rule
    public final ExpectedException exception = ExpectedException.none();

    @Before
    public void setUp() {
        expected = new Foo();
        expected.setName("name");
        expected.setValue(1);
        expected.setIgnored(true);
    }

    @Test
    public void testFromJsonInputStreamClassOfT()
            throws JsonGenerationException, JsonMappingException,
            IOException {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try {
            ObjectMappers.toJson(bytes, expected);
        } finally {
            bytes.close();
        }
        Foo actual;
        try (InputStream stream =
            new ByteArrayInputStream(bytes.toByteArray())) {
            actual = ObjectMappers.fromJson(stream, Foo.class);
        }
        assertEquals(expected.getName(), actual.getName());
        assertEquals(expected.getValue(), actual.getValue());
        assertFalse(actual.isIgnored());
    }

    @Test
    public void testFromJsonPathClassOfT()
            throws JsonGenerationException, JsonMappingException,
            IOException {
        Foo actual;
        Path file = createTempFile("test", ".json");
        try {
            ObjectMappers.toJson(file, expected);
            actual = ObjectMappers.fromJson(file, Foo.class);
        } finally {
            delete(file);
        }
        assertEquals(expected.getName(), actual.getName());
        assertEquals(expected.getValue(), actual.getValue());
        assertFalse(actual.isIgnored());
    }

    @Test
    public void testFromJsonReaderClassOfT()
            throws JsonParseException, JsonMappingException, IOException {
        StringWriter writer = new StringWriter();
        try {
            ObjectMappers.toJson(writer, expected);
        } finally {
            writer.close();
        }
        Foo actual;
        try (Reader reader = new StringReader(writer.toString())) {
            actual = ObjectMappers.fromJson(reader, Foo.class);
        }
        assertEquals(expected.getName(), actual.getName());
        assertEquals(expected.getValue(), actual.getValue());
        assertFalse(actual.isIgnored());
    }

    @Test
    public void testFromJsonStringClassOfT() throws IOException {
        String string = ObjectMappers.toJson(expected);
        Foo actual = ObjectMappers.fromJson(string, Foo.class);
        assertEquals(expected.getName(), actual.getName());
        assertEquals(expected.getValue(), actual.getValue());
        assertFalse(actual.isIgnored());
    }

    @Test
    public void testIntegerToJsonAndBackToInteger()
            throws JsonGenerationException, JsonMappingException,
            IOException, GeneralCryptoLibException {

        Integer originalValue = Integer.valueOf(10);

        String stringRepresentation = ObjectMappers.toJson(originalValue);

        TypeReference<Integer> typeReference = new TypeReference<Integer>() {};
        Integer recovered = ObjectMappers.fromJson(stringRepresentation, typeReference);

        String errorMsg = "The recovered list is not equal to the original list";
        assertEquals(errorMsg, originalValue, recovered);
    }
    
    @Test
    public void testListToJsonAndBackToList()
            throws JsonGenerationException, JsonMappingException,
            IOException, GeneralCryptoLibException {

        List<ZpGroupElement> originalList = getListOfGroupElements();

        String stringRepresentation = ObjectMappers.toJson(originalList);
        
        TypeReference<List<ZpGroupElement>> typeReference = new TypeReference<List<ZpGroupElement>>() {};
        List<ZpGroupElement> recovered = ObjectMappers.fromJson(stringRepresentation, typeReference);
        
        int expectedSize = 2;
        String errorMsg = "The recovered list does not have the expected size";
        assertEquals(errorMsg, expectedSize, recovered.size());

        errorMsg = "The recovered list is not equal to the original list";
        assertEquals(errorMsg, originalList, recovered);
    }

    @Test
    public void givenBadJsonStringWhenMapBackToListThenException()
            throws JsonGenerationException, JsonMappingException,
            IOException, GeneralCryptoLibException {

        exception.expect(JsonParseException.class);
        
        String badJsonString = "XXXXXX";
        
        TypeReference<List<ZpGroupElement>> typeReference = new TypeReference<List<ZpGroupElement>>() {};
        ObjectMappers.fromJson(badJsonString, typeReference);
    }

    @Test
    public void whenMismatchBetweenDataAndTypeRefThenException()
            throws JsonGenerationException, JsonMappingException,
            IOException, GeneralCryptoLibException {

        exception.expect(MismatchedInputException.class);

        List<ZpGroupElement> originalList = getListOfGroupElements();
        String stringRepresentation = ObjectMappers.toJson(originalList);
        
        TypeReference<List<Integer>> differentType = new TypeReference<List<Integer>>() {};
        ObjectMappers.fromJson(stringRepresentation, differentType);
    }

    @Test
    public void testReadCsvInputStreamClassOfTStrings()
            throws JsonGenerationException, JsonMappingException,
            IOException {
        try (InputStream stream =
            new ByteArrayInputStream(CSV.getBytes(StandardCharsets.UTF_8));
                MappingIterator<Foo> iterator = ObjectMappers
                    .readCsv(stream, Foo.class, "name", "value")) {
            assertTrue(iterator.hasNext());
            Foo foo = iterator.next();
            assertEquals("A", foo.getName());
            assertEquals(1, foo.getValue());
            assertFalse(foo.isIgnored());
            assertFalse(iterator.hasNext());
        }
    }

    @Test
    public void testReadCsvPathClassOfTStrings()
            throws JsonGenerationException, JsonMappingException,
            IOException {
        Path file = createTempFile("test", ".csv");
        try {
            write(file, CSV.getBytes(StandardCharsets.UTF_8));
            try (MappingIterator<Foo> iterator =
                ObjectMappers.readCsv(file, Foo.class, "name", "value")) {
                assertTrue(iterator.hasNext());
                Foo foo = iterator.next();
                assertEquals("A", foo.getName());
                assertEquals(1, foo.getValue());
                assertFalse(foo.isIgnored());
                assertFalse(iterator.hasNext());
            }
        } finally {
            delete(file);
        }
    }

    @Test
    public void testReadCsvReaderClassOfTStrings()
            throws JsonGenerationException, JsonMappingException,
            IOException {
        try (Reader stream = new StringReader(CSV);
                MappingIterator<Foo> iterator = ObjectMappers
                    .readCsv(stream, Foo.class, "name", "value")) {
            assertTrue(iterator.hasNext());
            Foo foo = iterator.next();
            assertEquals("A", foo.getName());
            assertEquals(1, foo.getValue());
            assertFalse(foo.isIgnored());
            assertFalse(iterator.hasNext());
        }
    }

    @Test
    public void testReadCsvStringClassOfTStrings()
            throws JsonGenerationException, JsonMappingException,
            IOException {
        try (MappingIterator<Foo> iterator =
            ObjectMappers.readCsv(CSV, Foo.class, "name", "value")) {
            assertTrue(iterator.hasNext());
            Foo foo = iterator.next();
            assertEquals("A", foo.getName());
            assertEquals(1, foo.getValue());
            assertFalse(foo.isIgnored());
            assertFalse(iterator.hasNext());
        }
    }

    public static final class Foo {
        private String name;

        private int value;

        private boolean ignored;

        public String getName() {
            return name;
        }

        public int getValue() {
            return value;
        }

        @JsonIgnore
        public boolean isIgnored() {
            return ignored;
        }

        public void setIgnored(final boolean ignored) {
            this.ignored = ignored;
        }

        public void setName(final String name) {
            this.name = name;
        }

        public void setValue(final int value) {
            this.value = value;
        }
    }
    
    private List<ZpGroupElement> getListOfGroupElements() throws GeneralCryptoLibException {

        BigInteger p = new BigInteger("23");
        BigInteger q = new BigInteger("11");
        BigInteger g = new BigInteger("2");
        ZpSubgroup group = new ZpSubgroup(g, p, q);
        List<ZpGroupElement> list = new ArrayList<>();
        list.add(new ZpGroupElement(new BigInteger("4"), group));
        list.add(new ZpGroupElement(new BigInteger("5"), group));
        return list;
    }
}
