/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.solr;

import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.apache.lucene.util.IOUtils;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.embedded.JettyConfig;
import org.apache.solr.client.solrj.embedded.JettySolrRunner;
import org.apache.solr.client.solrj.impl.NoOpResponseParser;
import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.response.UpdateResponse;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.CoreDescriptor;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.servlet.DirectSolrConnection;
import org.noggit.JSONUtil;
import org.noggit.ObjectBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@SolrTestCaseJ4.SuppressSSL
//@LuceneTestCase.SuppressCodecs({"Lucene3x","Lucene40","Lucene41","Lucene42","Lucene45","Appending","Asserting"})
public class SolrTestCaseHS extends SolrTestCaseJ4 {
  
  public static final String SOLR_TESTS_SHARDS_WHITELIST = "solr.tests.shardsWhitelist";

  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  @SafeVarargs
  public static <T> Set<T> set(T... a) {
    LinkedHashSet<T> s = new LinkedHashSet<>();
    for (T t : a) {
      s.add(t);
    }
    return s;
  }

  public static <T> T rand(T... vals) {
    return vals[ random().nextInt(vals.length) ];
  }


  public static ModifiableSolrParams params(SolrParams params, String... moreParams) {
    ModifiableSolrParams msp = new ModifiableSolrParams(params);
    for (int i=0; i<moreParams.length; i+=2) {
      msp.add(moreParams[i], moreParams[i+1]);
    }
    return msp;
  }

  public static Map<String,Object> toObject(Doc doc, IndexSchema schema, Collection<String> fieldNames) {
    Map<String,Object> result = new HashMap<>();
    for (Fld fld : doc.fields) {
      if (fieldNames != null && !fieldNames.contains(fld.ftype.fname)) continue;
      SchemaField sf = schema.getField(fld.ftype.fname);
      if (!sf.multiValued()) {
        result.put(fld.ftype.fname, fld.vals.get(0));
      } else {
        result.put(fld.ftype.fname, fld.vals);
      }
    }
    return result;
  }
  
  
  public static Object createDocObjects(Map<Comparable, Doc> fullModel, Comparator sort, int rows, Collection<String> fieldNames) {
    List<Doc> docList = new ArrayList<>(fullModel.values());
    Collections.sort(docList, sort);
    List sortedDocs = new ArrayList(rows);
    for (Doc doc : docList) {
      if (sortedDocs.size() >= rows) break;
      Map<String,Object> odoc = toObject(doc, h.getCore().getLatestSchema(), fieldNames);
      sortedDocs.add(toObject(doc, h.getCore().getLatestSchema(), fieldNames));
    }
    return sortedDocs;
  }


  public static void compare(SolrQueryRequest req, String path, Object model, Map<Comparable, Doc> fullModel) throws Exception {
    String strResponse = h.query(req);

    Object realResponse = ObjectBuilder.fromJSON(strResponse);
    String err = JSONTestUtil.matchObj(path, realResponse, model);
    if (err != null) {
      log.error("RESPONSE MISMATCH: " + err
              + "\n\trequest="+req
              + "\n\tresult="+strResponse
              + "\n\texpected="+ JSONUtil.toJSON(model)
              + "\n\tmodel="+ fullModel
      );

      // re-execute the request... good for putting a breakpoint here for debugging
      String rsp = h.query(req);

      fail(err);
    }

  }

  /** Pass "null" for the client to query the local server */
  public static void assertJQ(SolrClient client, SolrParams args, String... tests) throws Exception {
    String resp;
    resp = getJSON(client, args);
    matchJSON(resp, tests);
  }

  public static void matchJSON(String response, String... tests) throws Exception {
    boolean failed = false;

    for (String test : tests) {
      if (test == null || test.length()==0) continue;

      try {
        failed = true;
        String err = JSONTestUtil.match(response, test, JSONTestUtil.DEFAULT_DELTA);
        failed = false;
        if (err != null) {
          log.error("query failed JSON validation. error=" + err +
                  "\n expected =" + test +
                  "\n response = " + response
          );
          throw new RuntimeException(err);
        }
      } finally {
        if (failed) {
          log.error("JSON query validation threw an exception." +
                  "\n expected =" + test +
                  "\n response = " + response
          );
        }
      }
    }
  }

  /***
  public static void clearNCache() {
    SolrQueryRequest req = req();
    req.getSearcher().getnCache().clear();  // OFF-HEAP
    req.close();
  }***/

  public static void clearQueryCache() {
    SolrQueryRequest req = req();
    req.getSearcher();
    req.close();
  }


  public static String getQueryResponse(SolrClient client, String wt, SolrParams params) throws Exception {
    if (client == null) {
      return getQueryResponse(wt, params);
    }
    ModifiableSolrParams p = new ModifiableSolrParams(params);
    p.set("wt", wt);
    String path = p.get("qt");
    p.remove("qt");
    p.set("indent","true");

    QueryRequest query = new QueryRequest( p );
    if (path != null) {
      query.setPath(path);
    }

    query.setResponseParser(new NoOpResponseParser(wt));
    NamedList<Object> rsp = client.request(query);

    String raw = (String)rsp.get("response");

    return raw;
  }

  public static String getQueryResponse(String wt, SolrParams params) throws Exception {
    ModifiableSolrParams p = new ModifiableSolrParams(params);
    p.set("wt", wt);
    String path = p.get("qt");
    p.remove("qt");
    p.set("indent","true");

    DirectSolrConnection connection = new DirectSolrConnection(h.getCore());
    String raw = connection.request(path, p, null);
    return raw;
  }


  public static String getJSON(SolrClient client, SolrParams params) throws Exception {
    return getQueryResponse(client, "json", params);
  }

  /** Adds a document using the specific client, or to the local test core if null.
   * Returns the version.  TODO: work in progress... version not always returned.  */
  public static Long add(SolrClient client, SolrInputDocument sdoc, ModifiableSolrParams params) throws Exception {
    if (client == null) {
      Long version = addAndGetVersion( sdoc, params );
      return version;
    } else {
      UpdateRequest updateRequest = new UpdateRequest();
      if (params != null) {
        updateRequest.setParams(params);
      }
      updateRequest.add( sdoc );
      UpdateResponse rsp = updateRequest.process( client );
      // TODO - return version
      return null;
    }
  }



  public static class Client {
    ClientProvider provider;
    ModifiableSolrParams queryDefaults;
    public Tester tester = new Tester();

    public static class Tester {
      public  void assertJQ(SolrClient client, SolrParams args, String... tests) throws Exception {
        SolrTestCaseHS.assertJQ(client, args, tests);
      }
    }

    public static Client localClient = new Client(null, 1);
    public static Client localClient() {
      return new Client(null, 1);
    }

    public Client(List<SolrClient> clients, int seed) {
      if (clients != null) {
        provider = new ClientProvider(clients, seed);
      }
    }

    public static int hash(int x) {
      // from Thomas Mueller
      x = ((x >>> 16) ^ x) * 0x45d9f3b;
      x = ((x >>> 16) ^ x) * 0x45d9f3b;
      x = ((x >>> 16) ^ x);
      return x;
    }

    public ModifiableSolrParams queryDefaults() {
      if (queryDefaults == null) {
        queryDefaults = new ModifiableSolrParams();
      }
      return queryDefaults;
    }

    public boolean local() {
      return provider == null;
    }
    public ClientProvider getClientProvider() { return provider; }

    public void testJQ(SolrParams args, String... tests) throws Exception {
      if (queryDefaults != null) {
        ModifiableSolrParams newParams = params(queryDefaults);
        newParams.add(args);
        args = newParams;
      }
      SolrClient client = provider==null ? null : provider.client(null, args);
      tester.assertJQ(client, args, tests);
    }

    public Long add(SolrInputDocument sdoc, ModifiableSolrParams params) throws Exception {
      SolrClient client = provider==null ? null : provider.client(sdoc, params);
      return SolrTestCaseHS.add(client, sdoc, params);
    }

    public void commit() throws IOException, SolrServerException {
      if (local()) {
        assertU(SolrTestCaseJ4.commit());
        return;
      }

      for (SolrClient client : provider.all()) {
        client.commit();
      }
    }

    public void deleteByQuery(String query, ModifiableSolrParams params) throws IOException, SolrServerException {
      if (local()) {
        assertU(delQ(query));  // todo - handle extra params
        return;
      }

      for (SolrClient client : provider.all()) {
        client.deleteByQuery(query); // todo - handle extra params
      }
    }

  }


  public static class ClientProvider {
    public static String idField = "id";

    List<SolrClient> clients;
    Random r;
    int hashSeed;

    // thisIsIgnored needed because we need a diff signature
    public ClientProvider(List<SolrClient> clients, int seed) {
      this.hashSeed = Client.hash(seed);
      this.clients = clients;
      r = new Random(seed);
    }

    public SolrClient client(SolrInputDocument sdoc, SolrParams params) {
      String idStr = null;
      if (sdoc != null) {
        idStr = sdoc.getFieldValue(idField).toString();
      } else if (params!=null) {
        idStr = params.get(idField);
      }

      int hash;
      if (idStr != null) {
        // make the client chosen the same for a duplicate ID
        hash = idStr.hashCode() ^ hashSeed;
      } else {
        hash = r.nextInt();
      }


      return clients.get( (hash & Integer.MAX_VALUE) % clients.size() );
    }

    public List<SolrClient> all() {
      return clients;
    }

    public int getSeed() {
      return hashSeed;
    }
  }


  //
  // Helper to run an internal Jetty instance.
  // Example:
  //  SolrInstance s1 = new SolrInstance(createTempDir("s1"), "solrconfig-tlog.xml", "schema_latest.xml");
  //  s1.start();
  //  SolrClient c1 = s1.getSolrJ();
  //  assertJQ(c1, params("q", "*:*"), "/response/numFound==3");
  //  String json = getJSON(c1, params("q","id:1"));
  //  s1.stop();
  //
  // To manage multiple servers, see SolrInstances
  //
  public static class SolrInstance {
    private static Logger log = SolrTestCaseHS.log;
    private String collection = "collection1";
    private int port = 0;
    private String solrconfigFile;
    private String schemaFile;
    private File baseDir;
    private JettySolrRunner jetty;
    private SolrClient solrj;

    private boolean homeCreated = false;


    public SolrInstance(File homeDir, String solrconfigFile, String schemaFile) {
      this.baseDir = homeDir;
      this.solrconfigFile = solrconfigFile;
      this.schemaFile = schemaFile;
    }

    public String getBaseDir() {
      return baseDir.toString();
    }

    public String getBaseURL() {
      return (SolrTestCaseJ4.isSSLMode() ? "https" : "http") + "://127.0.0.1:" + port + "/solr";
    }

    public String getCollectionURL() {
      return getBaseURL() + "/" + collection;
    }

    /** string appropriate for passing in shards param (i.e. missing http://) */
    public String getShardURL() {
      return "127.0.0.1:" + port + "/solr" + "/" + collection;
    }

    public SolrClient getSolrJ() {
      if (solrj == null) {
        solrj = getHttpSolrClient(getCollectionURL());
      }
      return solrj;
    }

    /** If it needs to change */
    public void setPort(int port) {
      this.port = port;
    }

    public void createHome() throws Exception {
      homeCreated=true;
      SolrTestCaseJ4.copySolrHomeToTemp(baseDir, collection);
      copyConfFile(baseDir, collection, solrconfigFile);
      copyConfFile(baseDir, collection, schemaFile);

      File collDir = new File(baseDir, collection);
      try (Writer w = new OutputStreamWriter(Files.newOutputStream(collDir.toPath().resolve("core.properties")), StandardCharsets.UTF_8)) {
        Properties coreProps = new Properties();
        coreProps.put("name", "collection1");
        coreProps.put("config", solrconfigFile);
        coreProps.put("schema", schemaFile);
        coreProps.store(w, "");
      }
    }


    public void start() throws Exception {
      if (!homeCreated) {
        createHome();
      }

      if (jetty == null) {
        JettyConfig jettyConfig = JettyConfig.builder()
            .stopAtShutdown(true)
            .setContext("/solr")
            .setPort(port)
            .withSSLConfig(sslConfig.buildServerSSLConfig())
            .build();
        Properties nodeProperties = new Properties();
        nodeProperties.setProperty("solrconfig", solrconfigFile);
        nodeProperties.setProperty(CoreDescriptor.CORE_SCHEMA, schemaFile);
        jetty = new JettySolrRunner(baseDir.getAbsolutePath(), nodeProperties, jettyConfig);
      }

      // silly stuff included from solrconfig.snippet.randomindexconfig.xml
      System.setProperty("solr.tests.maxBufferedDocs", String.valueOf(100000));
      
      // If we want to run with whitelist list, this must be explicitly set to true for the test
      // otherwise we disable the check
      if (System.getProperty(SYSTEM_PROPERTY_SOLR_DISABLE_SHARDS_WHITELIST) == null) {
        systemSetPropertySolrDisableShardsWhitelist("true");
      }

      jetty.start();
      port = jetty.getLocalPort();
      log.info("===> Started solr server port=" + port + " home="+getBaseDir());
    }

    public void stop() throws Exception {
      jetty.stop();
      if (solrj != null) solrj.close();
    }

    public void tearDown() throws Exception {
      IOUtils.deleteFilesIfExist(baseDir.toPath());
    }

    private static void copyConfFile(File dstRoot, String destCollection, String file) throws Exception {
      File subHome = new File(dstRoot, destCollection + File.separator + "conf");
      String top = SolrTestCaseJ4.TEST_HOME() + "/collection1/conf";
      FileUtils.copyFile(new File(top, file), new File(subHome, file));
    }

    public void copyConfigFile(File dstRoot, String destCollection, String file) throws Exception {
      if (!homeCreated) {
        createHome();
      }

      File subHome = new File(dstRoot, destCollection + File.separator + "conf");
      String top = SolrTestCaseJ4.TEST_HOME() + "/collection1/conf";
      FileUtils.copyFile(new File(top, file), new File(subHome, file));
    }

  }


  //  Manages a number of Solr servers and provides a Client to partition documents and randomly assign query requests.
  //  Example:
  //        SolrInstances servers = new SolrInstances(3, "solrconfig-tlog.xml","schema_latest.xml");
  //        Client = servers.getClient(0);
  //        client.add(sdoc("id", "3"), null);
  //        client.commit();
  //        client.testJQ(params("q", "*:*"), "/response/numFound==3")
  //        servers.stop();
  //
  public static class SolrInstances {
    public List<SolrInstance> slist;
    public Client client;

    public SolrInstances(int numServers, String solrconfig, String schema) throws Exception {
      slist = new ArrayList<>(numServers);
      for (int i=0; i<numServers; i++) {
        SolrInstance instance = new SolrInstance(createTempDir("s"+ i).toFile(), solrconfig, schema);
        slist.add(instance);
        instance.start();
      }
    }

    public void stop() throws Exception {
      for (SolrInstance instance : slist) {
        instance.stop();
      }
    }

    // For params.set("shards", getShards())
    public String getShards() {
      return getShardsParam(slist);
    }
    
    public String getWhitelistString() {
      StringBuilder sb = new StringBuilder();
      boolean first = true;
      for (SolrInstance instance : slist) {
        if (first) {
          first = false;
        } else {
          sb.append(',');
        }
        sb.append( instance.getBaseURL().replace("/solr", ""));
      }
      return sb.toString();
    }

    public List<SolrClient> getSolrJs() {
      List<SolrClient> solrjs = new ArrayList<>(slist.size());
      for (SolrInstance instance : slist) {
        solrjs.add( instance.getSolrJ() );
      }
      return solrjs;
    }

    public Client getClient(int seed) {
      if (client == null) {
        client = new Client(getSolrJs(), seed);
      }
      return client;
    }

    public static String getShardsParam(List<SolrInstance> instances) {
      StringBuilder sb = new StringBuilder();
      boolean first = true;
      for (SolrInstance instance : instances) {
        if (first) {
          first = false;
        } else {
          sb.append(',');
        }
        sb.append( instance.getShardURL() );
      }
      return sb.toString();
    }



  }

}
