package io.vertx.mysqlclient.data;

import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.VertxUnitRunner;
import io.vertx.mysqlclient.MySQLConnection;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.Tuple;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.function.Consumer;

@RunWith(VertxUnitRunner.class)
public class JsonBinaryCodecTest extends JsonDataTypeTest {
  private static final String CREATE_TABLE = "CREATE TEMPORARY TABLE test_json\n" +
    "(\n" +
    "    json JSON\n" +
    ");";
  private static final String INSERT_JSON_WITHOUT_CAST = "INSERT INTO test_json VALUES (?);";
  private static final String INSERT_JSON_WITH_CAST = "INSERT INTO test_json VALUES (CAST(? AS JSON));";
  private static final String QUERY_JSON = "SELECT json FROM test_json";

  @Test
  public void testEncodeNumber(TestContext ctx) {
    testEncodeJsonWithCast(ctx, Tuple.of(12345), 12345, row -> {
      ctx.assertEquals(12345, row.getInteger(0));
      ctx.assertEquals(12345, row.getInteger("json"));
    });
  }

  @Test
  public void testEncodeString(TestContext ctx) {
    testEncodeJsonWithCast(ctx, Tuple.of("\"hello, world\""), "hello, world", row -> {
      ctx.assertEquals("hello, world", row.getString(0));
      ctx.assertEquals("hello, world", row.getString("json"));
    });
  }

  @Test
  public void testEncodeJsonLiteralNull(TestContext ctx) {
    testEncodeJson(ctx, Tuple.of(Tuple.JSON_NULL), Tuple.JSON_NULL, null, INSERT_JSON_WITHOUT_CAST);
  }

  @Test
  public void testEncodeSqlNull(TestContext ctx) {
    testEncodeJsonWithCast(ctx, Tuple.of(null), null, null);
  }

  @Test
  public void testEncodeBoolean(TestContext ctx) {
    testEncodeJsonWithCast(ctx, Tuple.of(true), 1, row -> {
      ctx.assertEquals(true, row.getBoolean(0));
      ctx.assertEquals(true, row.getBoolean("json"));
    });
  }

  @Test
  public void testEncodeFullJsonObject(TestContext ctx) {
    JsonObject expected = new JsonObject()
      .put("test_string", "hello")
      .put("test_number", 12345)
      .put("test_boolean", true)
      .put("test_null", (Object) null)
      .put("test_json_object", new JsonObject().put("key", "value"))
      .put("test_json_array", new JsonArray().add(1).add(2).add(3));
    testEncodeJson(ctx, Tuple.of(expected), expected, row -> ctx.assertEquals(expected, row.get(JsonObject.class, 0)),
      INSERT_JSON_WITHOUT_CAST);
  }

  @Test
  public void testEncodeJsonObject(TestContext ctx) {
    JsonObject expected = new JsonObject()
      .put("test_string", "hello")
      .put("test_number", 12345)
      .put("test_boolean", true)
      .put("test_null", (Object) null)
      .put("test_json_object", new JsonObject().put("key", "value"))
      .put("test_json_array", new JsonArray().add(1).add(2).add(3));
    testEncodeJson(ctx, Tuple.of("hello", 12345, null), expected, row -> ctx.assertEquals(expected, row.get(JsonObject.class, 0)),
      "INSERT INTO test_json VALUES (JSON_OBJECT(\n" +
        "               'test_string', ?,\n" +
        "               'test_number', ?,\n" +
        "               'test_boolean', true,\n" + // MySQL does not know it's a boolean
        "               'test_null', ?,\n" +
        "               'test_json_object', JSON_OBJECT('key', 'value'),\n" +
        "               'test_json_array', JSON_ARRAY(1, 2, 3)\n" +
        "           ))");
  }

  @Test
  public void testEncodeFullJsonArray(TestContext ctx) {
    JsonArray expected = new JsonArray()
      .add("hello")
      .add(12345)
      .add(true)
      .add((Object) null)
      .add(new JsonObject().put("key", "value"))
      .add(new JsonArray().add(1).add(2).add(3));
    testEncodeJson(ctx, Tuple.of(expected), expected, row -> ctx.assertEquals(expected, row.get(JsonArray.class, 0)),
      INSERT_JSON_WITHOUT_CAST);
  }

  @Test
  public void testEncodeJsonArray(TestContext ctx) {
    JsonArray expected = new JsonArray()
      .add("hello")
      .add(12345)
      .add(true)
      .add((Object) null)
      .add(new JsonObject().put("key", "value"))
      .add(new JsonArray().add(1).add(2).add(3));
    testEncodeJson(ctx, Tuple.of("hello", 12345, null), expected, row -> ctx.assertEquals(expected, row.get(JsonArray.class, 0)),
      "INSERT INTO test_json VALUES (JSON_ARRAY(\n" +
        "               ?, ?, true, ?, JSON_OBJECT('key', 'value'), JSON_ARRAY(1, 2, 3)\n" +
        "           ))");
  }

  @Override
  protected void testDecodeJson(TestContext ctx, String script, Object expected, Consumer<Row> checker) {
    MySQLConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> {
      conn.preparedQuery(script).execute(ctx.asyncAssertSuccess(result -> {
        ctx.assertEquals(1, result.size());
        Row row = result.iterator().next();
        ctx.assertEquals(expected, row.getValue(0));
        ctx.assertEquals(expected, row.getValue("json"));
        if (checker != null) {
          checker.accept(row);
        }
        conn.close();
      }));
    }));
  }

  private void testEncodeJsonWithCast(TestContext ctx, Tuple params, Object expected, Consumer<Row> checker) {
    testEncodeJson(ctx, params, expected, checker, INSERT_JSON_WITH_CAST);
  }

  private void testEncodeJson(TestContext ctx, Tuple params, Object expected, Consumer<Row> checker, String insertScript) {
    MySQLConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> {
      conn.preparedQuery(CREATE_TABLE).execute(ctx.asyncAssertSuccess(createTable -> {
        conn.preparedQuery(insertScript).execute(params, ctx.asyncAssertSuccess(insert -> {
          conn.preparedQuery(QUERY_JSON).execute(ctx.asyncAssertSuccess(result -> {
            ctx.assertEquals(1, result.size());
            Row row = result.iterator().next();
            ctx.assertEquals(expected, row.getValue(0));
            ctx.assertEquals(expected, row.getValue("json"));
            if (checker != null) {
              checker.accept(row);
            }
            conn.close();
          }));
        }));
      }));
    }));
  }
}
