package com.googlecode.totallylazy.json;

import com.googlecode.totallylazy.Assert;
import com.googlecode.totallylazy.Value;
import com.googlecode.totallylazy.collections.Keyword;
import com.googlecode.totallylazy.time.Dates;
import org.junit.Test;

import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.NoSuchElementException;

import static com.googlecode.totallylazy.Assert.assertThat;
import static com.googlecode.totallylazy.Assert.fail;
import static com.googlecode.totallylazy.Maps.map;
import static com.googlecode.totallylazy.collections.PersistentList.constructors.list;
import static com.googlecode.totallylazy.json.Json.json;
import static com.googlecode.totallylazy.json.JsonRecord.create;
import static com.googlecode.totallylazy.json.JsonRecord.parse;
import static com.googlecode.totallylazy.predicates.Predicates.is;
import static com.googlecode.totallylazy.predicates.Predicates.nullValue;
import static com.googlecode.totallylazy.time.Dates.date;

public class JsonRecordTest {
    @Test
    public void doesNotSerialiseNullValues() throws Exception {
        assertThat(new User().toString(), is("{}"));
    }

    static class User extends JsonRecord {
        String name;
        BigDecimal age;
    }

    @Test
    public void canCreateARecordFromAMap() throws Exception {
        User user = create(User.class, map("name", "Dan", "age", new BigDecimal(1)));
        assertThat(user.name, is("Dan"));
        assertThat(user.age, is(new BigDecimal(1)));
    }

    @Test
    public void canParseARecordFromJson() throws Exception {
        User user = parse(User.class, json(map("name", "Dan", "age", 1)));
        assertThat(user.name, is("Dan"));
        assertThat(user.age, is(new BigDecimal(1)));
    }

    @Test
    public void canConvertToJsonAndBack() throws Exception {
        User user = new User() {{
            name = "Dan";
            age = new BigDecimal(1);
        }};
        String json = user.toString();
        assertThat(json, is("{\"name\":\"Dan\",\"age\":1}"));
        User parsed = parse(User.class, json);
        assertThat(parsed.name, is(user.name));
        assertThat(parsed.age, is(user.age));
    }

    @Test
    public void jsonRecordsAreMaps() throws Exception {
        User user = new User() {{
            name = "Dan";
            age = new BigDecimal(1);
        }};
        Keyword<String> name = Keyword.keyword();
        assertThat(name.call(user), is("Dan"));
    }

    static class UserDocument extends JsonRecord {
        User user;
    }

    @Test
    public void supportsNestedRecords() throws Exception {
        UserDocument doc = new UserDocument() {{
            user = new User() {{
                name = "Dan";
                age = new BigDecimal(1);
            }};
        }};

        String json = doc.toString();
        assertThat(json, is("{\"user\":{\"name\":\"Dan\",\"age\":1}}"));

        UserDocument parsed = parse(UserDocument.class, json);
        assertThat(parsed.user, is(doc.user));
    }

    static class Users extends JsonRecord {
        List<User> users;
    }

    @Test
    public void supportsListsOfRecords() throws Exception {
        Users doc = new Users() {{
            users = list(new User() {{
                name = "Dan";
                age = new BigDecimal(1);
            }});
        }};

        String json = doc.toString();
        assertThat(json, is("{\"users\":[{\"name\":\"Dan\",\"age\":1}]}"));

        Users parsed = parse(Users.class, json);
        assertThat(parsed.users.size(), is(doc.users.size()));
        assertThat(parsed.users.get(0).name, is(doc.users.get(0).name));
        assertThat(parsed.users.get(0).age, is(doc.users.get(0).age));
    }

    @Test
    public void preservesUnknownAttributes() throws Exception {
        User user = create(User.class, map("name", "Dan", "age", new BigDecimal(1), "tel", "12345678890"));
        assertThat(user.name, is("Dan"));
        assertThat(user.age, is(new BigDecimal(1)));
        assertThat(user.get("tel"), is("12345678890"));
        String json = user.toString();
        assertThat(json, is("{\"name\":\"Dan\",\"age\":1,\"tel\":\"12345678890\"}"));
        User parsed = parse(User.class, json);
        assertThat(parsed.name, is(user.name));
        assertThat(parsed.age, is(user.age));
        assertThat(parsed.get("tel"), is("12345678890"));
    }

    static class IntUser extends JsonRecord {
        int age;
    }

    static class IntegerUser extends JsonRecord {
        Integer age;
    }

    @Test
    public void canCoerceIntegerTypes() throws Exception {
        assertThat(parse(IntUser.class, "{\"age\":1}").age, is(1));
        assertThat(parse(IntegerUser.class, "{\"age\":1}").age, is(1));
        assertThat(parse(IntegerUser.class, "{\"age\":null}").age, nullValue());
    }

    static class longUser extends JsonRecord {
        long age;
    }

    static class LongUser extends JsonRecord {
        Long age;
    }

    @Test
    public void canCoerceLongsTypes() throws Exception {
        assertThat(parse(longUser.class, "{\"age\":1}").age, is(1L));
        assertThat(parse(LongUser.class, "{\"age\":1}").age, is(1L));
        assertThat(parse(LongUser.class, "{\"age\":null}").age, nullValue());
    }

    enum Position {
        Long,
    }

    static class Trade extends JsonRecord {
        Position position;
    }

    @Test
    public void supportsEnums() throws Exception {
        assertThat(parse(Trade.class, "{\"position\":\"Long\"}").position, is(Position.Long));
    }

    enum CustomPosition {
        Short;

        static CustomPosition customPosition(String value) {
            if (value.equalsIgnoreCase("short")) return CustomPosition.Short;
            throw new UnsupportedOperationException();
        }
    }

    static class CustomTrade extends JsonRecord {
        CustomPosition position;
    }

    @Test
    public void supportsCustomEnumsFactoryMethods() throws Exception {
        assertThat(parse(CustomTrade.class, "{\"position\":\"short\"}").position, is(CustomPosition.Short));
    }

    interface Breed extends Value<String> {
        static Breed breed(String value) {
            return () -> value;
        }
    }

    static class Cat extends JsonRecord {
        Breed breed;
    }

    @Test
    public void supportsSimpleTypes() throws Exception {
        assertThat(parse(Cat.class, "{\"breed\":\"Tabby\"}").breed.value(), is("Tabby"));
    }

    @Test
    public void throwsUsefulErrorsWhenUnableToCoerceIntoType() throws Exception {
        try {
            parse(Cat.class, "{\"breed\":1}");
            fail("Expected exception");
        }
        catch (NoSuchElementException e) {
            Assert.assertThat(e.getMessage(), (s) -> s.matches(".*Breed.*BigDecimal"));
        }
    }

    interface Count extends Value<Integer> {
        static Count count(Number value) {
            return value::intValue;
        }
    }

    static class Crowd extends JsonRecord {
        Count count;
    }

    @Test
    public void supportsConstructorMethodsThatTakeSuperclasses() throws Exception {
        // 1 is a BigDecimal, but Count only has constructor method for Number
        String someJson = "{\"count\":1}";
        assertThat(parse(Crowd.class, someJson).count.value(), is(1));
    }

    static class Outer {
        private static class Inner extends JsonRecord {
        }
    }

    @Test
    public void canCreatePrivateJsonRecord() throws Exception {
        JsonRecord.create(Outer.Inner.class, map());
    }

    static class Account extends JsonRecord {
        public Date created_at;
    }

    @Test
    public void supportsDates() throws Exception {
        Date date = date(2001, 1, 1);
        assertThat(parse(Account.class, "{\"created_at\":\"" + Dates.RFC3339withMilliseconds().format(date) + "\"}").created_at, is(date));
    }

    static class Ledger extends JsonRecord {
        public List<Date> times;
    }

    @Test
    public void supportsListsOfCoerceables() throws Exception {
        Date date = date(2001, 1, 1);
        assertThat(parse(Ledger.class, "{\"times\":[\"" + Dates.RFC3339withMilliseconds().format(date) + "\"]}").times.get(0), is(date));

    }
}