package com.kaching.platform.testing;

import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Sets.newHashSet;
import static com.kaching.platform.testing.BadCodeSnippetsRunner.VerificationMode.BOTH;
import static java.lang.String.format;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.MapMaker;
import com.google.common.io.Files;

public class BadCodeSnippetsRunner extends AbstractDeclarativeTestRunner<BadCodeSnippetsRunner.CodeSnippets> {

  /**
   * Top level annotation used to describe the bad code snippet test.
   */
  @Target(TYPE)
  @Retention(RUNTIME)
  public @interface CodeSnippets {

    /**
     * Lists all the checks that must be performed by this bad code snippet
     * test.
     */
    public Check[] value();

    /**
     * Specifies the file extension to check for bad code snippet. The default
     * is {@code java}.
     */
    public String fileExtension() default "java";

  }

  @Retention(RUNTIME)
  @Target({})
  public @interface Check {

    public String[] paths();

    public Snippet[] snippets();

  }

  @Retention(RUNTIME)
  @Target({})
  public @interface Snippet {

    public String value();

    public String[] exceptions() default {};

    public VerificationMode verificationMode() default BOTH;

    public String rationale() default "(none provided)";

  }

  public enum VerificationMode {
    ONLY_MATCHES(false, true),
    ONLY_MISSING_MATCHES(true, false),
    BOTH(true, true);

    private final boolean reportMissing;
    private final boolean reportMatches;

    private VerificationMode(boolean missing, boolean matches) {
      this.reportMissing = missing;
      this.reportMatches = matches;
    }

  }

  /**
   * Internal use only.
   */
  public BadCodeSnippetsRunner(Class<?> klass) {
    super(klass, CodeSnippets.class);
  }

  @Override
  protected void runTest(CodeSnippets codeSnippets) throws IOException {
    for (Check check : codeSnippets.value()) {
      checkBadCodeSnippet(check, codeSnippets.fileExtension());
    }
  }

  private void checkBadCodeSnippet(
      Check check, String fileExtension) throws IOException {
    Map<Snippet, Set<File>> snippetsToUses = new MapMaker().makeComputingMap(
        new Function<Snippet, Set<File>>() {
          @Override
          public Set<File> apply(Snippet key) {
            return newHashSet();
          }});
    Map<Snippet, Pattern> compiledPatterns = new MapMaker().makeComputingMap(
        new Function<Snippet, Pattern>() {
          @Override
          public Pattern apply(Snippet key) {
            return Pattern.compile(key.value());
          }});

    Map<Snippet, Set<File>> snippetsToExceptions = snippetsToExceptions(check.snippets());
    Set<Snippet> snippets = snippetsToExceptions.keySet();
    for (String path : check.paths()) {
      collectUses(fileExtension, new File(path), snippets, snippetsToUses, compiledPatterns);
    }

    CombinedAssertionFailedError error =
        new CombinedAssertionFailedError("bad code uses");
    for (Snippet snippet : snippets) {
      Set<File> exceptions = snippetsToExceptions.get(snippet);
      Set<File> uses = snippetsToUses.get(snippet);
      List<File> spuriousExceptions = newArrayList(exceptions);
      spuriousExceptions.removeAll(uses);

      if (snippet.verificationMode().reportMissing && !spuriousExceptions.isEmpty()) {
        error.addError(format(
            "%s: marked as exception to snippet but didn't occur:\n    %s",
            snippet.value(), Joiner.on("\n   ").join(spuriousExceptions)));
        continue;
      }

      uses.removeAll(exceptions);
      if (snippet.verificationMode().reportMatches && !uses.isEmpty()) {
        error.addError(format(
            "%s: found %s bad snippets in:\n    %s\nrationale: %s",
            snippet.value(), uses.size(), Joiner.on("\n   ").join(uses), snippet.rationale()));
      }
    }

    error.throwIfHasErrors();
  }

  private Map<Snippet, Set<File>> snippetsToExceptions(Snippet[] snippets) {
    Map<Snippet, Set<File>> patternsToExceptions = newHashMap();
    for (Snippet snippet : snippets) {
      try {
        Set<File> files = newHashSet();
        for (int i = 0; i < snippet.exceptions().length; i++) {
          files.add(new File(snippet.exceptions()[i]));
        }
        patternsToExceptions.put(snippet, files);
      } catch (PatternSyntaxException e) {
        throw new AssertionError(e.getMessage());
      }
    }
    return patternsToExceptions;
  }

  private void collectUses(
      String fileExtension, File f, Iterable<Snippet> patterns,
      Map<Snippet, Set<File>> uses, Map<Snippet, Pattern> compiledPatterns)
      throws IOException {
    if (f.isFile()) {
      if (f.getName().endsWith("." + fileExtension)) {
        String code = Files.toString(f, UTF_8);
        for (Snippet p : patterns) {
          if (compiledPatterns.get(p).matcher(code).find()) {
            uses.get(p).add(f);
          }
        }
      }
    } else if (f.isDirectory()) {
      for (File c : f.listFiles()) {
        collectUses(fileExtension, c, patterns, uses, compiledPatterns);
      }
    }
  }

}

@@ -1885,23 +1885,8 @@
 lt %22
-(none provided)
 %22;%0A%0A
@@ -4221,32 +4221,154 @@
 es.isEmpty()) %7B%0A
+        String rationale = snippet.rationale().isEmpty() ? %22%22%0A            : format(%22rationale: %25s%22, snippet.rationale());%0A
         error.ad
@@ -4434,27 +4434,16 @@
     %25s%5Cn
-rationale: 
 %25s%22,%0A   
@@ -4512,24 +4512,16 @@
 (uses), 
-snippet.
 rational
@@ -4521,18 +4521,16 @@
 ationale
-()
 ));%0A    

Default rationale to empty string.