/*
 * Copyright 2017 The Apache Software Foundation.
 *
 * Licensed 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.avro.compiler.schema;

import org.apache.avro.Schema;
import org.apache.avro.SchemaCompatibility;
import org.junit.Assert;
import org.junit.Test;

public class TestSchemas {

  private static final String SCHEMA = "{\"type\":\"record\",\"name\":\"SampleNode\",\"doc\":\"caca\","
      + "\"namespace\":\"org.spf4j.ssdump2.avro\",\n" +
      " \"fields\":[\n" +
      "    {\"name\":\"count\",\"type\":\"int\",\"default\":0,\"doc\":\"caca\"},\n" +
      "    {\"name\":\"kind1\",\"type\":{\"type\":\"enum\", \"name\": \"Kind1\", \"symbols\": [\"A1\", \"B1\"]}},\n" +
      "    {\"name\":\"kind2\",\"type\":{\"type\":\"enum\", \"name\": \"Kind2\", \"symbols\": [\"A2\", \"B2\"], \"doc\": \"doc\"}},\n" +
      "    {\"name\":\"pat\",\"type\":{\"type\":\"fixed\", \"name\": \"FixedPattern\", \"size\": 10}},\n" +
      "    {\"name\":\"uni\",\"type\":[\"int\", \"double\"]},\n" +
      "    {\"name\":\"mp\",\"type\":{\"type\":\"map\", \"values\": \"int\"}},\n" +
      "    {\"name\":\"subNodes\",\"type\":\n" +
      "       {\"type\":\"array\",\"items\":{\n" +
      "           \"type\":\"record\",\"name\":\"SamplePair\",\n" +
      "           \"fields\":[\n" +
      "              {\"name\":\"method\",\"type\":\n" +
      "                  {\"type\":\"record\",\"name\":\"Method\",\n" +
      "                  \"fields\":[\n" +
      "                     {\"name\":\"declaringClass\",\"type\":{\"type\":\"string\",\"avro.java.string\":\"String\"}},\n" +
      "                     {\"name\":\"methodName\",\"type\":{\"type\":\"string\",\"avro.java.string\":\"String\"}}\n" +
      "                  ]}},\n" +
      "              {\"name\":\"node\",\"type\":\"SampleNode\"}]}}}" +
      "]}";

  private static class PrintingVisitor implements SchemaVisitor {


    @Override
    public SchemaVisitorAction visitTerminal(Schema terminal) {
      System.out.println("Terminal: " + terminal.getFullName());
      return SchemaVisitorAction.CONTINUE;
    }

    @Override
    public SchemaVisitorAction visitNonTerminal(Schema terminal) {
      System.out.println("NONTerminal start: " + terminal.getFullName());
      return SchemaVisitorAction.CONTINUE;
    }

    @Override
    public SchemaVisitorAction afterVisitNonTerminal(Schema terminal) {
      System.out.println("NONTerminal end: " + terminal.getFullName());
      return SchemaVisitorAction.CONTINUE;
    }

    @Override
    public Object get() {
      return null;
    }
  }


  @Test
  public void textCloning() {
    Schema recSchema = new Schema.Parser().parse(SCHEMA);
    Schemas.visit(recSchema, new PrintingVisitor());


    CloningVisitor cv = new CloningVisitor(recSchema);
    Schema trimmed = Schemas.visit(recSchema, cv);
    Assert.assertNull(trimmed.getDoc());
    Assert.assertNotNull(recSchema.getDoc());

    SchemaCompatibility.SchemaCompatibilityType compat =
        SchemaCompatibility.checkReaderWriterCompatibility(trimmed, recSchema).getType();
    Assert.assertEquals(SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE, compat);
    compat = SchemaCompatibility.checkReaderWriterCompatibility(recSchema, trimmed).getType();
    Assert.assertEquals(SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE, compat);
    Assert.assertNotNull(cv.toString());
  }

  @Test
  public void textCloningCopyDocs() {
    Schema recSchema = new Schema.Parser().parse(SCHEMA);
    Schemas.visit(recSchema, new PrintingVisitor());


    Schema trimmed = Schemas.visit(recSchema, new CloningVisitor(new CloningVisitor.PropertyCopier() {
      @Override
      public void copy(final Schema first, final Schema second) {
        Schemas.copyLogicalTypes(first, second);
        Schemas.copyAliases(first, second);
      }

      @Override
      public void copy(final Schema.Field first, final Schema.Field second) {
        Schemas.copyAliases(first, second);
      }
    }, true, recSchema));
    Assert.assertEquals("caca", trimmed.getDoc());
    Assert.assertNotNull(recSchema.getDoc());

    SchemaCompatibility.SchemaCompatibilityType compat =
        SchemaCompatibility.checkReaderWriterCompatibility(trimmed, recSchema).getType();
    Assert.assertEquals(SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE, compat);
    compat = SchemaCompatibility.checkReaderWriterCompatibility(recSchema, trimmed).getType();
    Assert.assertEquals(SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE, compat);
  }

  @Test(expected = IllegalStateException.class)
  public void testCloningError1() {
    // Visit Terminal with union
    Schema recordSchema = new Schema.Parser().parse(
        "{\"type\": \"record\", \"name\": \"R\", \"fields\":[{\"name\": \"f1\", \"type\": [\"int\", \"long\"]}]}");
    new CloningVisitor(recordSchema).visitTerminal(recordSchema.getField("f1").schema());
  }

  @Test(expected = IllegalStateException.class)
  public void testCloningError2() {
    // After visit Non-terminal with int
    Schema recordSchema = new Schema.Parser().parse(
        "{\"type\": \"record\", \"name\": \"R\", \"fields\":[{\"name\": \"f1\", \"type\": \"int\"}]}");
    new CloningVisitor(recordSchema).afterVisitNonTerminal(recordSchema.getField("f1").schema());
  }

  @Test
  public void testHasGeneratedJavaClass() {
    Assert.assertTrue(Schemas.hasGeneratedJavaClass(
        new Schema.Parser().parse("{\"type\": \"fixed\", \"name\": \"N\", \"size\": 10}")));
    Assert.assertFalse(Schemas.hasGeneratedJavaClass(new Schema.Parser().parse("{\"type\": \"int\"}")));
  }

  @Test
  public void testGetJavaClassName() {
    Assert.assertEquals("N", Schemas.getJavaClassName(
        new Schema.Parser().parse("{\"type\": \"fixed\", \"name\": \"N\", \"size\": 10}")));
    Assert.assertEquals("N", Schemas.getJavaClassName(
        new Schema.Parser().parse("{\"type\": \"fixed\", \"name\": \"N\", \"size\": 10, \"namespace\": \"\"}")));
    Assert.assertEquals("com.example.N", Schemas.getJavaClassName(
        new Schema.Parser().parse("{\"type\": \"fixed\", \"name\": \"N\", \"size\": 10, \"namespace\": \"com.example\"}")));
  }

  private static class TestVisitor implements SchemaVisitor<String> {
    StringBuilder sb = new StringBuilder();

    public SchemaVisitorAction visitTerminal(Schema terminal) {
      sb.append(terminal);
      return SchemaVisitorAction.CONTINUE;
    }

    public SchemaVisitorAction visitNonTerminal(Schema nonTerminal) {
      String n = nonTerminal.getName();
      sb.append(n).append('.');
      if (n.startsWith("t")) {
        return SchemaVisitorAction.TERMINATE;
      } else if (n.startsWith("ss")) {
        return SchemaVisitorAction.SKIP_SIBLINGS;
      } else if (n.startsWith("st")) {
        return SchemaVisitorAction.SKIP_SUBTREE;
      } else {
        return SchemaVisitorAction.CONTINUE;
      }
    }

    public SchemaVisitorAction afterVisitNonTerminal(Schema nonTerminal) {
      sb.append("!");
      String n = nonTerminal.getName();
      if (n.startsWith("ct")) {
        return SchemaVisitorAction.TERMINATE;
      } else if (n.startsWith("css")) {
        return SchemaVisitorAction.SKIP_SIBLINGS;
      } else if (n.startsWith("cst")) {
        return SchemaVisitorAction.SKIP_SUBTREE;
      } else {
        return SchemaVisitorAction.CONTINUE;
      }
    }

    public String get() {
      return sb.toString();
    }
  }

  @Test
  public void testVisit1() {
    String s1 = "{\"type\": \"record\", \"name\": \"t1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": \"int\"}" +
        "]}";
    Assert.assertEquals("t1.", Schemas.visit(new Schema.Parser().parse(s1), new TestVisitor()));
  }

  @Test
  public void testVisit2() {
    String s2 = "{\"type\": \"record\", \"name\": \"c1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": \"int\"}" +
        "]}";
    Assert.assertEquals("c1.\"int\"!", Schemas.visit(new Schema.Parser().parse(s2), new TestVisitor()));

  }

  @Test
  public void testVisit3() {
    String s3 = "{\"type\": \"record\", \"name\": \"ss1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": \"int\"}" +
        "]}";
    Assert.assertEquals("ss1.", Schemas.visit(new Schema.Parser().parse(s3), new TestVisitor()));

  }

  @Test
  public void testVisit4() {
    String s4 = "{\"type\": \"record\", \"name\": \"st1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": \"int\"}" +
        "]}";
    Assert.assertEquals("st1.", Schemas.visit(new Schema.Parser().parse(s4), new TestVisitor()));

  }

  @Test
  public void testVisit5() {
    String s5 = "{\"type\": \"record\", \"name\": \"c1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": {\"type\": \"record\", \"name\": \"c2\", \"fields\": " +
        "[{\"name\": \"f11\", \"type\": \"int\"}]}}," +
        "{\"name\": \"f2\", \"type\": \"long\"}" +
        "]}";
    Assert.assertEquals("c1.c2.\"int\"!\"long\"!",
        Schemas.visit(new Schema.Parser().parse(s5), new TestVisitor()));

  }

  @Test
  public void testVisit6() {
    String s6 = "{\"type\": \"record\", \"name\": \"c1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": {\"type\": \"record\", \"name\": \"ss2\", \"fields\": " +
        "[{\"name\": \"f11\", \"type\": \"int\"}]}}," +
        "{\"name\": \"f2\", \"type\": \"long\"}" +
        "]}";
    Assert.assertEquals("c1.ss2.!",
        Schemas.visit(new Schema.Parser().parse(s6), new TestVisitor()));

  }

  @Test
  public void testVisit7() {
    String s7 = "{\"type\": \"record\", \"name\": \"c1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": {\"type\": \"record\", \"name\": \"css2\", \"fields\": " +
        "[{\"name\": \"f11\", \"type\": \"int\"}]}}," +
        "{\"name\": \"f2\", \"type\": \"long\"}" +
        "]}";
    Assert.assertEquals("c1.css2.\"int\"!!",
        Schemas.visit(new Schema.Parser().parse(s7), new TestVisitor()));
  }

  @Test(expected = UnsupportedOperationException.class)
  public void testVisit8() {
    String s8 = "{\"type\": \"record\", \"name\": \"c1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": {\"type\": \"record\", \"name\": \"cst2\", \"fields\": " +
        "[{\"name\": \"f11\", \"type\": \"int\"}]}}," +
        "{\"name\": \"f2\", \"type\": \"int\"}" +
        "]}";
    Schemas.visit(new Schema.Parser().parse(s8), new TestVisitor());
  }

  @Test
  public void testVisit9() {
    String s9 = "{\"type\": \"record\", \"name\": \"c1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": {\"type\": \"record\", \"name\": \"ct2\", \"fields\": " +
        "[{\"name\": \"f11\", \"type\": \"int\"}]}}," +
        "{\"name\": \"f2\", \"type\": \"long\"}" +
        "]}";
    Assert.assertEquals("c1.ct2.\"int\"!", Schemas.visit(new Schema.Parser().parse(s9), new TestVisitor()));
  }


  @Test(expected = UnsupportedOperationException.class)
  public void testVisit10() {
    String s10 = "{\"type\": \"record\", \"name\": \"c1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": {\"type\": \"record\", \"name\": \"ct2\", \"fields\": " +
        "[{\"name\": \"f11\", \"type\": \"int\"}]}}," +
        "{\"name\": \"f2\", \"type\": \"int\"}" +
        "]}";
    Schemas.visit(new Schema.Parser().parse(s10),
        new TestVisitor() {
          public SchemaVisitorAction visitTerminal(Schema terminal) {
            return SchemaVisitorAction.SKIP_SUBTREE;
          }
        });
  }

  @Test
  public void testVisit11() {
    String s11 = "{\"type\": \"record\", \"name\": \"c1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": {\"type\": \"record\", \"name\": \"c2\", \"fields\": " +
        "[{\"name\": \"f11\", \"type\": \"int\"},{\"name\": \"f12\", \"type\": \"double\"}" +
        "]}}," +
        "{\"name\": \"f2\", \"type\": \"long\"}" +
        "]}";
    Assert.assertEquals("c1.c2.\"int\".!\"long\".!", Schemas.visit(new Schema.Parser().parse(s11),
        new TestVisitor() {
          public SchemaVisitorAction visitTerminal(Schema terminal) {
            sb.append(terminal).append('.');
            return SchemaVisitorAction.SKIP_SIBLINGS;
          }
        }));
  }


  @Test
  public void testVisit12() {
    String s12 = "{\"type\": \"record\", \"name\": \"c1\", \"fields\": [" +
        "{\"name\": \"f1\", \"type\": {\"type\": \"record\", \"name\": \"ct2\", \"fields\": " +
        "[{\"name\": \"f11\", \"type\": \"int\"}]}}," +
        "{\"name\": \"f2\", \"type\": \"long\"}" +
        "]}";
    Assert.assertEquals("c1.ct2.\"int\".", Schemas.visit(new Schema.Parser().parse(s12),
        new TestVisitor() {
          public SchemaVisitorAction visitTerminal(Schema terminal) {
            sb.append(terminal).append('.');
            return SchemaVisitorAction.TERMINATE;
          }
        }));
  }

  @Test
  public void testVisit13() {
    String s12 = "{\"type\": \"int\"}";
    Assert.assertEquals("\"int\".", Schemas.visit(new Schema.Parser().parse(s12),
        new TestVisitor() {
          public SchemaVisitorAction visitTerminal(Schema terminal) {
            sb.append(terminal).append('.');
            return SchemaVisitorAction.SKIP_SIBLINGS;
          }
        }));
  }
}