/*
 * Copyright (C) 2015 Pedro Vicente Gomez Sanchez.
 *
 * 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.github.pedrovgs.sample;

import android.test.ActivityInstrumentationTestCase2;
import android.view.View;
import android.widget.Adapter;
import android.widget.AdapterView;
import com.github.pedrovgs.lynx.model.Trace;
import com.github.pedrovgs.lynx.model.TraceLevel;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;

import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.clearText;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.hamcrest.core.IsNot.not;

/**
 * @author Pedro Vicente Gómez Sánchez.
 */
public class LynxActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {

  private static final String ANY_TRACE_FILTER = "Lynx";

  public LynxActivityTest() {
    super(MainActivity.class);
  }

  @Override protected void setUp() throws Exception {
    super.setUp();
    getActivity();
    onView(withId(R.id.bt_show_lynx_activity)).perform(click());
    onView(withId(R.id.et_filter)).perform(clearText());
  }

  public void testAppliesTraceFilterToShowsJustTracesMatchingFilter() {
    onView(withId(R.id.et_filter)).perform(typeText(ANY_TRACE_FILTER), closeSoftKeyboard());

    waitForSomeTraces();

    onData(allOf(is(instanceOf(Trace.class)),
        traceMatcherWithMessage(ANY_TRACE_FILTER))).inAdapterView(withId(R.id.lv_traces))
        .check(matches(isDisplayed()));
  }

  public void testShowsTracesEqualsOrGreaterThanVerboseTraceLevelOnTraceLevelSelected() {
    selectFilterByTraceLevel(TraceLevel.VERBOSE);

    waitForSomeTraces();

    assertShowsTraceMatchingTraceLevel(TraceLevel.VERBOSE);
    assertShowsTraceMatchingTraceLevel(TraceLevel.DEBUG);
    assertShowsTraceMatchingTraceLevel(TraceLevel.INFO);
    assertShowsTraceMatchingTraceLevel(TraceLevel.WARNING);
    assertShowsTraceMatchingTraceLevel(TraceLevel.ERROR);
    assertShowsTraceMatchingTraceLevel(TraceLevel.WTF);
  }

  public void testShowsTracesEqualsOrGreaterThanDebugTraceLevelOnTraceLevelSelected() {
    selectFilterByTraceLevel(TraceLevel.DEBUG);

    waitForSomeTraces();

    assertShowsTraceMatchingTraceLevel(TraceLevel.DEBUG);
    assertShowsTraceMatchingTraceLevel(TraceLevel.INFO);
    assertShowsTraceMatchingTraceLevel(TraceLevel.WARNING);
    assertShowsTraceMatchingTraceLevel(TraceLevel.ERROR);
    assertShowsTraceMatchingTraceLevel(TraceLevel.WTF);
    assertTracesListDoesNotShowTracesLowerThan(TraceLevel.DEBUG);
  }

  public void testShowsTracesEqualsOrGreaterThanInfoTraceLevelOnTraceLevelSelected() {
    selectFilterByTraceLevel(TraceLevel.INFO);

    waitForSomeTraces();

    assertShowsTraceMatchingTraceLevel(TraceLevel.INFO);
    assertShowsTraceMatchingTraceLevel(TraceLevel.WARNING);
    assertShowsTraceMatchingTraceLevel(TraceLevel.ERROR);
    assertShowsTraceMatchingTraceLevel(TraceLevel.WTF);
    assertTracesListDoesNotShowTracesLowerThan(TraceLevel.INFO);
  }

  public void testShowsTracesEqualsOrGreaterThanWARNINGTraceLevelOnTraceLevelSelected() {
    selectFilterByTraceLevel(TraceLevel.WARNING);

    waitForSomeTraces();

    assertShowsTraceMatchingTraceLevel(TraceLevel.WARNING);
    assertShowsTraceMatchingTraceLevel(TraceLevel.ERROR);
    assertShowsTraceMatchingTraceLevel(TraceLevel.WTF);
    assertTracesListDoesNotShowTracesLowerThan(TraceLevel.WARNING);
  }

  public void testShowsTracesEqualsOrGreaterThanErrorTraceLevelOnTraceLevelSelected() {
    selectFilterByTraceLevel(TraceLevel.ERROR);

    waitForSomeTraces();

    assertShowsTraceMatchingTraceLevel(TraceLevel.ERROR);
    assertShowsTraceMatchingTraceLevel(TraceLevel.WTF);
    assertTracesListDoesNotShowTracesLowerThan(TraceLevel.ERROR);
  }

  public void testShowsTracesEqualsOrGreaterThanWtfRTraceLevelOnTraceLevelSelected() {
    selectFilterByTraceLevel(TraceLevel.WTF);

    waitForSomeTraces();

    assertShowsTraceMatchingTraceLevel(TraceLevel.WTF);
    assertTracesListDoesNotShowTracesLowerThan(TraceLevel.WTF);
  }

  private void selectFilterByTraceLevel(TraceLevel traceLevel) {
    onView(withId(R.id.sp_filter)).perform(click());
    onData(allOf(is(instanceOf(TraceLevel.class)), is(equalTo(traceLevel)))).perform(click());
  }

  private void assertShowsTraceMatchingTraceLevel(TraceLevel traceLevel) {
    onData(allOf(is(instanceOf(Trace.class)), traceMatcherWithLevel(traceLevel))).inAdapterView(
        withId(R.id.lv_traces)).check(matches(isDisplayed()));
  }

  private Matcher<Object> traceMatcherWithLevel(final TraceLevel traceLevel) {
    return new BaseMatcher<Object>() {
      private boolean hasPreviousMatch;

      @Override public boolean matches(Object o) {
        if (o instanceof Trace && !hasPreviousMatch) {
          Trace trace = (Trace) o;
          if (trace.getLevel().equals(traceLevel)) {
            hasPreviousMatch = true;
            return true;
          }
        }
        return false;
      }

      @Override public void describeTo(Description description) {
        description.appendText("Trace has '")
            .appendText(traceLevel.name())
            .appendText("' as TraceLevel.");
      }
    };
  }

  private Matcher<Object> traceMatcherWithMessage(final String message) {
    return new BaseMatcher<Object>() {
      private boolean hasPreviousMatch;

      @Override public boolean matches(Object o) {
        if (o instanceof Trace && !hasPreviousMatch) {
          Trace trace = (Trace) o;
          if (trace.getMessage().contains(message) && !hasPreviousMatch) {
            hasPreviousMatch = true;
            return true;
          }
        }
        return false;
      }

      @Override public void describeTo(Description description) {
        description.appendText("Trace contains '").appendText(message + "' in trace message.");
      }
    };
  }

  private void assertTracesListDoesNotShowTracesLowerThan(TraceLevel traceLevel) {
    onView(withId(R.id.lv_traces)).check(matches(not(withTraceLevelInAdapter(traceLevel))));
  }

  private static Matcher<View> withTraceLevelInAdapter(final TraceLevel traceLevel) {
    return new TypeSafeMatcher<View>() {

      @Override public void describeTo(Description description) {
        description.appendText(
            "Your ListView contains at least one trace with TraceLevel less than TraceLevel: ");
        description.appendText(String.valueOf(traceLevel));
      }

      @Override public boolean matchesSafely(View view) {
        if (!(view instanceof AdapterView)) {
          return false;
        }
        @SuppressWarnings("rawtypes") Adapter adapter = ((AdapterView) view).getAdapter();
        for (int i = 0; i < adapter.getCount(); i++) {
          Trace trace = (Trace) adapter.getItem(i);
          if (trace.getLevel().ordinal() < traceLevel.ordinal()) {
            return true;
          }
        }
        return false;
      }
    };
  }

  /**
   * Ugly sleep used for some of this tests. This method is needed because we can't provide Log
   * traces from the test application process and the default traces generation is implemented in
   * MainActivity. onData method doesn't wait until different log traces are displayed with the
   * filter provided, that's why I need this Thread.Sleep. Please, don't do this at home.
   */
  private void waitForSomeTraces() {
    try {
      Thread.sleep(5000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}