/*
 * Jooby https://jooby.io
 * Apache License Version 2.0 https://jooby.io/LICENSE.txt
 * Copyright 2014 Edgar Espina
 */
package io.jooby.javadoc;

import static org.junit.jupiter.api.Assertions.*;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.function.Consumer;

import org.junit.jupiter.api.Test;

import io.jooby.javadoc.input.*;
import io.jooby.javadoc.input.sub.Base;

public class JavaDocParserTest {

  @Test
  public void inheritance() throws Exception {
    withDoc(
        Subclass.class,
        doc -> {
          assertEquals("Subclass summary.", doc.getSummary());
          assertEquals("Subclass description.", doc.getDescription());

          assertEquals("Number on subclass.", doc.getPropertyDoc("number"));
          assertEquals("Name on subclass.", doc.getPropertyDoc("name"));
          assertEquals("Desc on base class.", doc.getPropertyDoc("description"));
        });

    withDoc(
        Base.class,
        doc -> {
          assertEquals("Base summary.", doc.getSummary());
          assertEquals("Base description.", doc.getDescription());

          assertNull(doc.getPropertyDoc("number"));
          assertEquals("Name on base class.", doc.getPropertyDoc("name"));
          assertEquals("Desc on base class.", doc.getPropertyDoc("description"));
        });
  }

  @Test
  public void multiline() throws Exception {
    withDoc(
        MultilineComment.class,
        doc -> {
          assertNull(doc.getSummary());
          assertNull(doc.getDescription());

          withScript(
              doc,
              "GET",
              "/multiline",
              method -> {
                assertEquals("Multiline comment.", method.getSummary());
                assertEquals("multilineComment", method.getOperationId());
                assertEquals("Description in next line.", method.getDescription());
                assertNull(method.getReturnDoc());
                assertEquals("Path ID.", method.getParameterDoc("id"));
              });
        });
  }

  @Test
  public void lambdaDoc() throws Exception {
    withDoc(
        LambdaRefApp.class,
        doc -> {
          assertEquals("LambdaRefApp", doc.getSimpleName());
          assertEquals("io.jooby.javadoc.input.LambdaRefApp", doc.getName());
          assertEquals("Lambda App.", doc.getSummary());
          assertEquals("Using method ref.", doc.getDescription());

          withScript(
              doc,
              "GET",
              "/reference",
              method -> {
                assertEquals("Find pet by id.", method.getSummary());
                assertEquals("findPetById", method.getOperationId());
                assertNull(method.getDescription());
                assertNull(method.getReturnDoc());
                assertEquals("Pet ID.", method.getParameterDoc("id"));
              });

          withScript(
              doc,
              "POST",
              "/static-reference",
              method -> {
                assertEquals("Static reference.", method.getSummary());
                assertEquals("staticFindPetById", method.getOperationId());
                assertEquals("Path ID.", method.getParameterDoc("id"));
                assertEquals("Description in next line.", method.getDescription());
                assertNull(method.getReturnDoc());
              });

          withScript(
              doc,
              "PUT",
              "/external-reference",
              method -> {
                assertEquals("External doc.", method.getSummary());
                assertEquals("external", method.getOperationId());
                assertNull(method.getDescription());
                assertNull(method.getReturnDoc());
              });

          withScript(
              doc,
              "GET",
              "/external-subPackage-reference",
              method -> {
                assertEquals("Sub package doc.", method.getSummary());
                assertEquals("subPackage", method.getOperationId());
                assertNull(method.getDescription());
                assertNull(method.getReturnDoc());
              });
        });
  }

  @Test
  public void scriptDoc() throws Exception {
    withDoc(
        ScriptApp.class,
        doc -> {
          assertEquals("ScriptApp", doc.getSimpleName());
          assertEquals("io.jooby.javadoc.input.ScriptApp", doc.getName());
          assertEquals("Script App.", doc.getSummary());
          assertEquals("Some description.", doc.getDescription());

          withScript(
              doc,
              "GET",
              "/static",
              method -> {
                assertEquals("This is a static path.", method.getSummary());
                assertEquals("No parameters", method.getDescription());
                assertEquals("Request Path.", method.getReturnDoc());
              });

          withScript(
              doc,
              "DELETE",
              "/{id}",
              method -> {
                assertEquals("Delete something.", method.getSummary());
                assertEquals("ID to delete.", method.getParameterDoc("id"));
              });

          withScript(
              doc,
              "GET",
              "/{id}",
              method -> {
                assertEquals("Path param.", method.getSummary());
                assertNull(method.getDescription());
                assertEquals("Some value.", method.getReturnDoc());
                assertEquals("Path ID.", method.getParameterDoc("id"));
              });

          withScript(
              doc,
              "GET",
              "/tree/folder/{id}",
              method -> {
                assertNotNull(method.getPath());
                assertEquals("Tree summary.", method.getPath().getSummary());
                assertEquals("Tree doc.", method.getPath().getDescription());
                assertNotNull(method.getPath().getTags());
                assertEquals(1, method.getPath().getTags().size());
                assertEquals("Tree", method.getPath().getTags().getFirst().name());

                assertEquals("Item doc.", method.getSummary());
                assertNull(method.getDescription());
              });

          withScript(
              doc,
              "GET",
              "/tree/folder",
              method -> {
                assertEquals("Items.", method.getSummary());
                assertNull(method.getDescription());
              });

          withScript(
              doc,
              "GET",
              "/tree/file/{fileId}",
              method -> {
                assertEquals("Sub Items.", method.getSummary());
                assertNull(method.getDescription());
              });

          withScript(
              doc,
              "GET",
              "/tree/mount",
              method -> {
                assertEquals("Mounted.", method.getSummary());
                assertNull(method.getDescription());
              });

          withScript(
              doc,
              "POST",
              "/routes",
              method -> {
                assertEquals("Routes.", method.getSummary());
                assertNull(method.getDescription());
              });
          withScript(
              doc,
              "GET",
              "/nested/last",
              method -> {
                assertEquals("Last.", method.getSummary());
                assertNull(method.getDescription());
              });
        });
  }

  @Test
  public void apiDoc() throws Exception {
    withDoc(
        ApiDoc.class,
        doc -> {
          assertEquals("ApiDoc", doc.getSimpleName());
          assertEquals("io.jooby.javadoc.input.ApiDoc", doc.getName());
          assertEquals("Api summary.", doc.getSummary());
          assertEquals(
              "Proin sit amet lectus interdum, porta libero quis, fringilla metus. Integer viverra"
                  + " ante id vestibulum congue. Nam et tortor at magna tempor congue.",
              doc.getDescription());

          var servers = doc.getServers();
          assertEquals(2, servers.size());
          assertEquals("https://api.example.com/v1", servers.get(0).get("url"));
          assertEquals("Production server (uses live data)", servers.get(0).get("description"));
          assertEquals("https://sandbox-api.example.com:8443/v1", servers.get(1).get("url"));
          assertEquals("Sandbox server (uses test data)", servers.get(1).get("description"));

          withMethod(
              doc,
              "hello",
              List.of("java.util.List", "int", "java.util.List", "java.lang.String"),
              method -> {
                assertEquals("This is the Hello /endpoint", method.getSummary());
                assertEquals("Operation description", method.getDescription());
                assertEquals("Person name.", method.getParameterDoc("name"));
                assertEquals("Person age. Multi line doc.", method.getParameterDoc("age"));
                assertEquals("This line has a break.", method.getParameterDoc("list"));
                assertEquals("Some string.", method.getParameterDoc("str"));
                assertEquals("Welcome message 200.", method.getReturnDoc());
              });

          withMethod(
              doc,
              "search",
              List.of("io.jooby.javadoc.input.QueryBeanDoc"),
              method -> {
                assertEquals("Search database.", method.getSummary());
                assertEquals("Search DB", method.getDescription());
                assertNull(method.getParameterDoc("fq"));
                assertNull(method.getParameterDoc("offset"));
                assertNull(method.getParameterDoc("limit"));
                assertNull(method.getReturnDoc());
              });

          withMethod(
              doc,
              "recordBean",
              List.of("io.jooby.javadoc.input.RecordBeanDoc"),
              method -> {
                assertEquals("Record database.", method.getSummary());
                assertNull(method.getDescription());
                assertNull(method.getParameterDoc("id"));
                assertNull(method.getParameterDoc("name"));
              });

          withMethod(
              doc,
              "enumParam",
              List.of("io.jooby.javadoc.input.EnumDoc"),
              method -> {
                assertEquals("Enum database.", method.getSummary());
                assertEquals("Enum doc.", method.getParameterDoc("query"));
              });
        });
  }

  @Test
  public void ignoreStatementComment() throws Exception {
    var result = newParser().parse("issues.i1580.Controller1580");
    assertTrue(result.isEmpty());
  }

  @Test
  public void noDoc() throws Exception {
    var result = newParser().parse("io.jooby.javadoc.input.NoDoc");
    assertTrue(result.isEmpty());
  }

  @Test
  public void noClassDoc() throws Exception {
    withDoc(
        NoClassDoc.class,
        doc -> {
          assertNull(doc.getSummary());
          assertNull(doc.getDescription());

          withMethod(
              doc,
              "hello",
              List.of("java.lang.String"),
              methodDoc -> {
                assertEquals("Method Doc.", methodDoc.getSummary());
                assertNull(methodDoc.getDescription());
              });
        });
  }

  @Test
  public void shouldParseEnum() throws Exception {
    withDoc(
        EnumDoc.class,
        doc -> {
          assertEquals("Enum summary.", doc.getSummary());
          assertEquals("Enum desc.", doc.getDescription());
          assertEquals(
              "Enum summary.\n" + "  - Foo: Foo doc.\n" + "  - Bar: Bar doc.",
              doc.getEnumDescription(doc.getSummary()));
        });
  }

  @Test
  public void shouldParseBean() throws Exception {
    withDoc(
        Book.class,
        doc -> {
          assertEquals("Book model.", doc.getSummary());
          assertNull(doc.getDescription());

          // bean like
          assertEquals("Book's title.", doc.getPropertyDoc("title"));
        });

    withDoc(
        QueryBeanDoc.class,
        doc -> {
          assertEquals("Search options.", doc.getSummary());
          assertNull(doc.getDescription());

          withMethod(
              doc,
              "getFq",
              List.of(),
              methodDoc -> {
                assertEquals("Filter query.", methodDoc.getSummary());
                assertEquals("Works like internal filter.", methodDoc.getDescription());
              });

          // bean like
          assertEquals("Filter query. Works like internal filter.", doc.getPropertyDoc("fq"));
          withField(
              doc,
              "fq",
              field -> {
                assertEquals("The field comment.", field.getSummary());
              });
          assertEquals("Offset, used for paging.", doc.getPropertyDoc("offset"));
        });
  }

  @Test
  public void shouldRecord() throws Exception {
    withDoc(
        RecordBeanDoc.class,
        doc -> {
          assertEquals("Record documentation.", doc.getSummary());
          assertNull(doc.getDescription());

          withMethod(
              doc,
              "id",
              List.of(),
              methodDoc -> {
                assertEquals("Person id.", methodDoc.getSummary());
                assertEquals("Unique person identifier.", methodDoc.getDescription());
              });

          // bean like
          assertEquals("Person id. Unique person identifier.", doc.getPropertyDoc("id"));
          withField(
              doc,
              "id",
              field -> {
                assertEquals("Person id.", field.getSummary());
                assertEquals("Unique person identifier.", field.getDescription());
                ;
              });
        });
  }

  @Test
  public void shouldVerifyJavaDocScope() throws Exception {
    withDoc(
        ScopeDoc.class,
        doc -> {
          assertEquals("Class", doc.getSummary());
          assertNull(doc.getDescription());

          withMethod(
              doc,
              "getName",
              List.of(),
              methodDoc -> {
                assertEquals("Method", methodDoc.getSummary());
                assertNull(methodDoc.getDescription());
              });

          withField(
              doc,
              "name",
              methodDoc -> {
                assertEquals("Field", methodDoc.getSummary());
                assertNull(methodDoc.getDescription());
              });
        });
  }

  private JavaDocParser newParser() {
    return new JavaDocParser(baseDir());
  }

  private Path baseDir() {
    return Paths.get(System.getProperty("user.dir")).resolve("src").resolve("test").resolve("java");
  }

  private void withDoc(Class<?> typeName, Consumer<ClassDoc> consumer) throws Exception {
    try {
      var result = newParser().parse(typeName.getName());
      assertFalse(result.isEmpty());
      consumer.accept(result.get());
    } catch (Throwable cause) {
      throw Utils.propagate(cause);
    }
  }

  private void withMethod(
      ClassDoc doc, String name, List<String> types, Consumer<MethodDoc> consumer) {
    var method = doc.getMethod(name, types);
    assertTrue(method.isPresent());
    consumer.accept(method.get());
  }

  private void withScript(
      ClassDoc doc, String method, String pattern, Consumer<ScriptDoc> consumer) {
    var script = doc.getScript(method, pattern);
    assertTrue(script.isPresent());
    consumer.accept(script.get());
  }

  private void withField(ClassDoc doc, String name, Consumer<FieldDoc> consumer) {
    var method = doc.getField(name);
    assertTrue(method.isPresent());
    consumer.accept(method.get());
  }
}
