package io.anemos.metastore.core.proto.validation;

import com.google.protobuf.Descriptors;
import io.anemos.metastore.core.Lint;
import io.anemos.metastore.core.proto.validate.ProtoLint;
import io.anemos.metastore.core.proto.validate.ValidationResults;
import io.anemos.metastore.core.test.v1.VersionScopeInvalid;
import io.anemos.metastore.core.test.v1.VersionScopeValid;
import io.anemos.metastore.invalid.Invalid;
import io.anemos.metastore.putils.ProtoDomain;
import io.anemos.metastore.unused.UsedValidImport;
import io.anemos.metastore.unused.invalid.UnusedInvalidImport;
import io.anemos.metastore.v1alpha1.*;
import java.io.IOException;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;

public class LintTest {

  @Test
  public void fieldOkSnakeCase() throws IOException {
    Report result = lintMessage(Lint.LintFieldNamesGood.getDescriptor());
    Assert.assertEquals(0, result.getMessageResultsCount());
  }

  @Test
  public void fieldNokCamel() throws IOException {
    Report result = lintMessage(Lint.LintFieldNamesBad.getDescriptor());
    Assert.assertEquals(1, result.getMessageResultsCount());

    MessageResult mr = result.getMessageResultsOrThrow("anemos.metastore.core.LintFieldNamesBad");
    assertOnField(mr, 1, LintRule.LINT_FIELD_NAME_SHOULD_BE_SNAKE, "L20001/02");
    assertOnField(mr, 2, LintRule.LINT_FIELD_NAME_SHOULD_BE_SNAKE, "L20001/01");
    assertOnField(mr, 3, LintRule.LINT_FIELD_NAME_SHOULD_BE_SNAKE, "L20001/05");
  }

  @Test
  public void messageLowerCase() throws IOException {
    Report result = lintMessage(Lint.lintmessagelowercase.getDescriptor());
    Assert.assertEquals(1, result.getMessageResultsCount());

    MessageResult mr =
        result.getMessageResultsOrThrow("anemos.metastore.core.lintmessagelowercase");
    assertOnMessage(mr, LintRule.LINT_MESSAGE_NAME_SHOULD_BE_PASCAL, "L10001/00");
  }

  @Test
  public void messageCamelCase() throws IOException {
    Report result = lintMessage(Lint.lint_message_camelcase.getDescriptor());
    Assert.assertEquals(1, result.getMessageResultsCount());

    MessageResult mr =
        result.getMessageResultsOrThrow("anemos.metastore.core.lint_message_camelcase");
    assertOnMessage(mr, LintRule.LINT_MESSAGE_NAME_SHOULD_BE_PASCAL, "L10001/00");
  }

  @Test
  public void methodInputAndReturnTypeNoRR() throws IOException {
    Report result =
        lintService(Lint.LintFieldNamesGood.getDescriptor(), "anemos.metastore.core.MethodService");

    ServiceResult mr = result.getServiceResultsOrThrow("anemos.metastore.core.MethodService");
    Assert.assertEquals(2, getInfoForMethod(mr, "MethodEmpty").size());
    assertOnMethod(mr, "MethodEmpty", LintRule.LINT_METHOD_RTYPE_END_WITH_RESPONSE, "L50003/00");
    assertOnMethod(mr, "MethodEmpty", LintRule.LINT_METHOD_ITYPE_END_WITH_REQUEST, "L50002/00");
  }

  @Test
  public void methodInputTypeNoR() throws IOException {
    Report result =
        lintService(Lint.LintFieldNamesGood.getDescriptor(), "anemos.metastore.core.MethodService");

    ServiceResult mr = result.getServiceResultsOrThrow("anemos.metastore.core.MethodService");
    Assert.assertEquals(1, getInfoForMethod(mr, "MethodEmptyI").size());
    assertOnMethod(mr, "MethodEmptyI", LintRule.LINT_METHOD_ITYPE_END_WITH_REQUEST, "L50002/00");
  }

  @Test
  public void methodReturnTypeNoR() throws IOException {
    Report result =
        lintService(Lint.LintFieldNamesGood.getDescriptor(), "anemos.metastore.core.MethodService");

    ServiceResult mr = result.getServiceResultsOrThrow("anemos.metastore.core.MethodService");
    Assert.assertEquals(1, getInfoForMethod(mr, "MethodEmptyR").size());
    assertOnMethod(mr, "MethodEmptyR", LintRule.LINT_METHOD_RTYPE_END_WITH_RESPONSE, "L50003/00");
  }

  @Test
  public void methodOk() throws IOException {
    Report result =
        lintService(Lint.LintFieldNamesGood.getDescriptor(), "anemos.metastore.core.MethodService");

    ServiceResult mr = result.getServiceResultsOrThrow("anemos.metastore.core.MethodService");
    Assert.assertNull(getInfoForMethod(mr, "MethodOk"));
  }

  private Report lintMessage(Descriptors.Descriptor d) throws IOException {
    ProtoDomain pd = ProtoDomain.buildFrom(d);
    String message = d.getFullName();

    ValidationResults results = new ValidationResults();
    ProtoLint lint = new ProtoLint(pd, results);
    lint.lintOnMessage(message);
    return results.createProto();
  }

  private Report lintService(Descriptors.Descriptor ref, String name) throws IOException {
    ProtoDomain pd = ProtoDomain.buildFrom(ref);

    ValidationResults results = new ValidationResults();
    ProtoLint lint = new ProtoLint(pd, results);
    lint.lintOnService(name);

    return results.createProto();
  }

  private List<RuleInfo> getInfoForField(MessageResult mr, int fieldNumber) {
    for (FieldResult fieldResult : mr.getFieldResultsList()) {
      if (fieldResult.getNumber() == fieldNumber) {
        return fieldResult.getInfoList();
      }
    }
    Assert.assertNotNull(null);
    return null;
  }

  private List<RuleInfo> getInfoForMethod(ServiceResult mr, String methodName) {
    for (MethodResult methodResult : mr.getMethodResultsList()) {
      if (methodResult.getName().equals(methodName)) {
        return methodResult.getInfoList();
      }
    }
    return null;
  }

  private void assertOnMethod(
      ServiceResult mr, String methodName, LintRule expecredRule, String expectedCode) {
    List<RuleInfo> infoForField = getInfoForMethod(mr, methodName);
    String code = null;
    LintRule rule = null;
    for (RuleInfo ruleInfo : infoForField) {
      if (ruleInfo.getCode().equals(expectedCode) && ruleInfo.getLintRule().equals(expecredRule)) {
        return;
      }
      code = ruleInfo.getCode();
      rule = ruleInfo.getLintRule();
    }
    Assert.assertEquals(expectedCode, code);
    Assert.assertEquals(expecredRule, rule);
  }

  private void assertOnField(
      MessageResult mr, int fieldNumber, LintRule expecredRule, String expectedCode) {
    List<RuleInfo> infoForField = getInfoForField(mr, fieldNumber);
    String code = null;
    LintRule rule = null;
    for (RuleInfo ruleInfo : infoForField) {
      if (ruleInfo.getCode().equals(expectedCode) && ruleInfo.getLintRule().equals(expecredRule)) {
        return;
      }
      code = ruleInfo.getCode();
      rule = ruleInfo.getLintRule();
    }
    Assert.assertEquals(expectedCode, code);
    Assert.assertEquals(expecredRule, rule);
  }

  private void assertOnMessage(MessageResult mr, LintRule expectedRule, String expectedCode) {
    String code = null;
    LintRule rule = null;
    for (RuleInfo ruleInfo : mr.getInfoList()) {
      if (ruleInfo.getCode().equals(expectedCode) && ruleInfo.getLintRule().equals(expectedRule)) {
        return;
      }
      code = ruleInfo.getCode();
      rule = ruleInfo.getLintRule();
    }
    Assert.assertEquals(expectedCode, code);
    Assert.assertEquals(expectedRule, rule);
  }

  @Test
  public void packageScopeVersionValid() throws IOException {
    Descriptors.Descriptor descriptor = Lint.LintFieldNamesBad.getDescriptor();
    Report result = lintPackage(descriptor);
    Assert.assertEquals(0, result.getFileResultsCount());
  }

  @Test
  public void packageScopeVersionInvalid() throws IOException {
    Descriptors.Descriptor descriptor = Invalid.InvalidMessage.getDescriptor();
    Report result = lintPackage(descriptor);
    FileResult fr = result.getFileResultsOrThrow("anemos/metastore/invalid/invalid.proto");

    Assert.assertEquals(1, result.getFileResultsCount());
    assertOnFile(fr, LintRule.LINT_PACKAGE_NO_DIR_ALIGNMENT, "L70001/00");
  }

  private void assertOnFile(FileResult fr, LintRule expectedRule, String expectedCode) {
    String code = null;
    LintRule rule = null;
    for (RuleInfo ruleInfo : fr.getInfoList()) {
      if (ruleInfo.getCode().equals(expectedCode) && ruleInfo.getLintRule().equals(expectedRule)) {
        return;
      }
      code = ruleInfo.getCode();
      rule = ruleInfo.getLintRule();
    }
    Assert.assertEquals(expectedCode, code);
    Assert.assertEquals(expectedRule, rule);
  }

  private Report lintPackage(Descriptors.Descriptor ref) throws IOException {
    ProtoDomain pd = ProtoDomain.buildFrom(ref);
    ValidationResults results = new ValidationResults();

    ProtoLint lint = new ProtoLint(pd, results);
    lint.lintOnPackage(ref.getFile());
    return results.createProto();
  }

  @Test
  public void versionScopeValid() throws IOException {
    Descriptors.Descriptor descriptor = VersionScopeValid.VersionValid.getDescriptor();
    Report result = lintVersion(descriptor);
    Assert.assertEquals(0, result.getFileResultsCount());
  }

  @Test
  public void versionScopeInvalid() throws IOException {
    Descriptors.Descriptor descriptor = VersionScopeInvalid.VersionInValid.getDescriptor();
    Report result = lintVersion(descriptor);
    FileResult fr =
        result.getFileResultsOrThrow("anemos/metastore/core/test/v1/version_scope_invalid.proto");

    Assert.assertEquals(1, result.getFileResultsCount());
    assertOnFile(fr, LintRule.LINT_PACKAGE_NO_VERSION_ALIGNMENT, "L70002/00");
  }

  private Report lintVersion(Descriptors.Descriptor ref) throws IOException {
    ProtoDomain pd = ProtoDomain.buildFrom(ref);
    ValidationResults results = new ValidationResults();

    ProtoLint lint = new ProtoLint(pd, results);
    lint.lintOnVersion(ref);
    return results.createProto();
  }

  @Test
  public void unusedImportValid() throws IOException {
    Descriptors.Descriptor descriptor = UsedValidImport.Valid.getDescriptor();
    Report result = lintImport(descriptor);
    Assert.assertEquals(0, result.getFileResultsCount());
  }

  @Test
  public void unusedImportInvalid() throws IOException {
    Descriptors.Descriptor descriptor = UnusedInvalidImport.Invalid.getDescriptor();
    Report result = lintImport(descriptor);
    FileResult fr =
        result.getFileResultsOrThrow("anemos/metastore/unused/invalid/unused_invalid_import.proto");
    Assert.assertEquals(1, result.getFileResultsCount());
    assertOnFile(fr, LintRule.LINT_IMPORT_NO_ALIGNMENT, "L80001/00");
  }

  private Report lintImport(Descriptors.Descriptor ref) throws IOException {
    ProtoDomain pd = ProtoDomain.buildFrom(ref);
    ValidationResults results = new ValidationResults();

    ProtoLint lint = new ProtoLint(pd, results);
    lint.lintOnImport(ref);
    return results.createProto();
  }
}