// Copyright (C) 2011 The Android Open Source Project
//
// 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.gerrit.server.rules;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Guice;
import com.google.inject.Module;
import com.googlecode.prolog_cafe.exceptions.CompileException;
import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.PrologClassLoader;
import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
import com.googlecode.prolog_cafe.lang.StructureTerm;
import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
import com.googlecode.prolog_cafe.lang.VariableTerm;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Ignore;

/** Base class for any tests written in Prolog. */
@Ignore
public abstract class PrologTestCase {
  private static final SymbolTerm test_1 = SymbolTerm.intern("test", 1);

  private String pkg;
  private boolean hasSetup;
  private boolean hasTeardown;
  private List<Term> tests;
  protected PrologMachineCopy machine;
  protected PrologEnvironment.Factory envFactory;

  protected void load(String pkg, String prologResource, Module... modules)
      throws CompileException, IOException {
    ArrayList<Module> moduleList = new ArrayList<>();
    moduleList.add(new PrologModule.EnvironmentModule());
    moduleList.addAll(Arrays.asList(modules));

    envFactory = Guice.createInjector(moduleList).getInstance(PrologEnvironment.Factory.class);
    PrologEnvironment env = envFactory.create(newMachine());
    consult(env, getClass(), prologResource);

    this.pkg = pkg;
    hasSetup = has(env, "setup");
    hasTeardown = has(env, "teardown");

    StructureTerm head =
        new StructureTerm(
            ":", SymbolTerm.intern(pkg), new StructureTerm(test_1, new VariableTerm()));

    tests = new ArrayList<>();
    for (Term[] pair : env.all(Prolog.BUILTIN, "clause", head, new VariableTerm())) {
      tests.add(pair[0]);
    }
    assertThat(tests).isNotEmpty();
    machine = PrologMachineCopy.save(env);
  }

  /**
   * Set up the Prolog environment.
   *
   * @param env Prolog environment.
   */
  protected void setUpEnvironment(PrologEnvironment env) throws Exception {}

  private PrologMachineCopy newMachine() {
    BufferingPrologControl ctl = new BufferingPrologControl();
    ctl.setMaxDatabaseSize(16 * 1024);
    ctl.setPrologClassLoader(new PrologClassLoader(getClass().getClassLoader()));
    return PrologMachineCopy.save(ctl);
  }

  protected void consult(BufferingPrologControl env, Class<?> clazz, String prologResource)
      throws CompileException, IOException {
    try (InputStream in = clazz.getResourceAsStream(prologResource)) {
      if (in == null) {
        throw new FileNotFoundException(prologResource);
      }
      SymbolTerm pathTerm = SymbolTerm.create(prologResource);
      JavaObjectTerm inTerm =
          new JavaObjectTerm(
              new PushbackReader(
                  new BufferedReader(new InputStreamReader(in, UTF_8)), Prolog.PUSHBACK_SIZE));
      if (!env.execute(Prolog.BUILTIN, "consult_stream", pathTerm, inTerm)) {
        throw new CompileException("Cannot consult " + prologResource);
      }
    }
  }

  private boolean has(BufferingPrologControl env, String name) {
    StructureTerm head = SymbolTerm.create(pkg, name, 0);
    return env.execute(Prolog.BUILTIN, "clause", head, new VariableTerm());
  }

  public void runPrologBasedTests() throws Exception {
    int errors = 0;
    long start = TimeUtil.nowMs();

    for (Term test : tests) {
      PrologEnvironment env = envFactory.create(machine);
      setUpEnvironment(env);
      env.setEnabled(Prolog.Feature.IO, true);

      System.out.format("Prolog %-60s ...", removePackage(test));
      System.out.flush();

      if (hasSetup) {
        call(env, "setup");
      }

      List<Term> all = env.all(Prolog.BUILTIN, "call", test);

      if (hasTeardown) {
        call(env, "teardown");
      }

      System.out.println(all.size() == 1 ? "OK" : "FAIL");

      if (all.size() > 0 && !test.equals(all.get(0))) {
        for (Term t : all) {
          Term head = ((StructureTerm) removePackage(t)).args()[0];
          Term[] args = ((StructureTerm) head).args();
          System.out.print("  Result: ");
          for (int i = 0; i < args.length; i++) {
            if (0 < i) {
              System.out.print(", ");
            }
            System.out.print(args[i]);
          }
          System.out.println();
        }
        System.out.println();
      }

      if (all.size() != 1) {
        errors++;
      }
    }

    long end = TimeUtil.nowMs();
    System.out.println("-------------------------------");
    System.out.format(
        "Prolog tests: %d, Failures: %d, Time elapsed %.3f sec",
        tests.size(), errors, (end - start) / 1000.0);
    System.out.println();

    assertThat(errors).isEqualTo(0);
  }

  private void call(BufferingPrologControl env, String name) {
    StructureTerm head = SymbolTerm.create(pkg, name, 0);
    assertWithMessage("Cannot invoke " + pkg + ":" + name)
        .that(env.execute(Prolog.BUILTIN, "call", head))
        .isTrue();
  }

  private Term removePackage(Term test) {
    Term name = test;
    if (name instanceof StructureTerm && ":".equals(name.name())) {
      name = name.arg(1);
    }
    return name;
  }
}
