/*
 * Copyright 2008 Google Inc.
 * 
 * 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 com.google.gwt.dev.util;

import com.google.gwt.core.ext.TreeLogger;

import junit.framework.Assert;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

/**
 * A {@link TreeLogger} implementation that can be used during JUnit tests to
 * check for a specified sequence of log events.
 */
public class UnitTestTreeLogger extends TreeLogger {

  /**
   * Simplifies the creation of a {@link UnitTestTreeLogger} by providing
   * convenience methods for specifying the expected log events.
   */
  public static class Builder {

    private final List<LogEntry> expected = new ArrayList<LogEntry>();
    private EnumSet<Type> loggableTypes = EnumSet.allOf(TreeLogger.Type.class);

    public Builder() {
    }

    public UnitTestTreeLogger createLogger() {
      return new UnitTestTreeLogger(expected, loggableTypes);
    }

    public void expect(TreeLogger.Type type, String msg,
        Class<? extends Throwable> caught) {
      expected.add(new LogEntry(type, msg, caught));
    }

    public void expectDebug(String msg, Class<? extends Throwable> caught) {
      expect(TreeLogger.DEBUG, msg, caught);
    }

    public void expectError(String msg, Class<? extends Throwable> caught) {
      expect(TreeLogger.ERROR, msg, caught);
    }

    public void expectInfo(String msg, Class<? extends Throwable> caught) {
      expect(TreeLogger.INFO, msg, caught);
    }

    public void expectSpam(String msg, Class<? extends Throwable> caught) {
      expect(TreeLogger.SPAM, msg, caught);
    }

    public void expectTrace(String msg, Class<? extends Throwable> caught) {
      expect(TreeLogger.TRACE, msg, caught);
    }

    public void expectWarn(String msg, Class<? extends Throwable> caught) {
      expect(TreeLogger.WARN, msg, caught);
    }

    /**
     * Sets the loggable types based on an explicit set.
     */
    public void setLoggableTypes(EnumSet<TreeLogger.Type> loggableTypes) {
      this.loggableTypes = loggableTypes;
    }

    /**
     * Sets the loggable types based on a lowest log level.
     */
    public void setLowestLogLevel(TreeLogger.Type lowestLogLevel) {
      loggableTypes.clear();
      for (Type type : TreeLogger.Type.values()) {
        if (!type.isLowerPriorityThan(lowestLogLevel)) {
          loggableTypes.add(type);
        }
      }
    }
  }

  /**
   * Represents a log event to check for.
   */
  private static class LogEntry {
    private final Class<? extends Throwable> caught;
    private final String msg;
    private final Type type;

    public LogEntry(TreeLogger.Type type, String msg,
        Class<? extends Throwable> caught) {
      assert (type != null);
      this.type = type;
      this.msg = msg;
      this.caught = caught;
    }

    public LogEntry(Type type, String msg, Throwable caught) {
      this(type, msg, (caught == null) ? null : caught.getClass());
    }

    public Class<? extends Throwable> getCaught() {
      return caught;
    }

    public String getMessage() {
      return msg;
    }

    public Type getType() {
      return type;
    }

    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder();
      sb.append(type.getLabel());
      sb.append(": ");
      sb.append(getMessage());
      Class<? extends Throwable> t = getCaught();
      if (t != null) {
        sb.append("; ");
        sb.append(t.getName());
      }
      return sb.toString();
    }
  }

  private static void assertCorrectLogEntry(LogEntry expected, LogEntry actual) {
    Assert.assertEquals("Log types do not match", expected.getType(),
        actual.getType());
    Assert.assertEquals("Log messages do not match", expected.getMessage(),
        actual.getMessage());
    if (expected.getCaught() == null) {
      Assert.assertNull("Actual log exception type should have been null",
          actual.getCaught());
    } else {
      Assert.assertNotNull(
          "Actual log exception type should not have been null",
          actual.getCaught());
      Assert.assertTrue("Actual log exception type ("
          + actual.getCaught().getName()
          + ") cannot be assigned to expected log exception type ("
          + expected.getCaught().getName() + ")",
          expected.getCaught().isAssignableFrom(actual.getCaught()));
    }
    Assert.assertEquals("Log types do not match", expected.getType(),
        actual.getType());
  }

  private final List<LogEntry> actualEntries = new ArrayList<LogEntry>();
  private final List<LogEntry> expectedEntries = new ArrayList<LogEntry>();

  private final EnumSet<TreeLogger.Type> loggableTypes;

  public UnitTestTreeLogger(List<LogEntry> expectedEntries,
      EnumSet<TreeLogger.Type> loggableTypes) {
    this.expectedEntries.addAll(expectedEntries);
    this.loggableTypes = loggableTypes;

    // Sanity check that all expected entries are actually loggable.
    for (LogEntry entry : expectedEntries) {
      Type type = entry.getType();
      Assert.assertTrue("Cannot expect an entry of a non-loggable type!",
          isLoggable(type));
      loggableTypes.add(type);
    }
  }

  public void assertCorrectLogEntries() {
    if (expectedEntries.size() != actualEntries.size()) {
      Assert.fail("Wrong log count: expected=" + expectedEntries + ", actual=" + actualEntries);
    }
    for (int i = 0, c = expectedEntries.size(); i < c; ++i) {
      assertCorrectLogEntry(expectedEntries.get(i), actualEntries.get(i));
    }
  }

  @Override
  public TreeLogger branch(Type type, String msg, Throwable caught,
      HelpInfo helpInfo) {
    log(type, msg, caught);
    return this;
  }

  @Override
  public boolean isLoggable(Type type) {
    return loggableTypes.contains(type);
  }

  @Override
  public void log(Type type, String msg, Throwable caught, HelpInfo helpInfo) {
    if (!isLoggable(type)) {
      return;
    }
    LogEntry actualEntry = new LogEntry(type, msg, caught);
    actualEntries.add(actualEntry);
  }
}
