package com.sonalake.utah.config;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

/**
 * A test of the configuration classes
 */
public class ConfigTests {

  /**
   * We build up an XML doc in this and then generate it into a string before
   * parsing it with the config
   */
  private Document document;

  @Before
  public void setup() throws ParserConfigurationException {
    document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
  }

  /**
   * If the config has no delimiter, then fail out
   */
  @Test(expected = IllegalArgumentException.class)
  public void testConfigHasNoDelimiter() throws TransformerException, IOException {
    createEmptyDocument();
    new ConfigLoader().loadConfig(buildDocReader());
  }

  /**
   * If the config has values with no groups, then fail out
   */
  @Test(expected = IllegalArgumentException.class)
  public void testConfigMappingHasNoGroup() throws TransformerException, IOException {
    createEmptyDocument();
    addDelimiter("DELIM");
    addSearch("number", "\\d*");
    addValue("value", "{number}");
    new ConfigLoader().loadConfig(buildDocReader());

  }

  /**
   * If the config has values with no groups, then fail out
   */
  @Test(expected = IllegalArgumentException.class)
  public void testConfigMappingWhenInvalidRegex() throws TransformerException, IOException {
    createEmptyDocument();
    addDelimiter("DELIM");
    addSearch("number", "(\\d*)");
    addValue("value", "{number}(");
    new ConfigLoader().loadConfig(buildDocReader());
  }

  /**
   * Create a valid document that can handle values and then confirm it with parsing
   */
  @Test
  public void testValidValueConfigOk() throws TransformerException, IOException {
    createEmptyDocument();
    addDelimiter("DELIM");
    addSearch("number", "(\\d*)");
    addValue("value", "a value {number}");
    Config config = new ConfigLoader().loadConfig(buildDocReader());

    Map<String, String> record = config.buildRecord("this is a value 123 hello");
    assertEquals("123", record.get("value"));
  }

  /**
   * Create a valid document that can handle values and then confirm it with parsing
   */
  @Test
  public void testValidHeaderValueConfigOk() throws TransformerException, IOException {
    createEmptyDocument();
    addDelimiter("DELIM");
    addSearch("number", "(\\d*)");
    addHeaderValue("value", "a value {number}");
    Config config = new ConfigLoader().loadConfig(buildDocReader());

    Map<String, String> record = config.buildHeader("this is a value 123 hello");
    assertEquals("123", record.get("value"));
  }

  /**
   * Create a valid document that can handle values and then confirm it with parsing
   */
  @Test
  public void testHeaderDelimiterValueConfigOk() throws TransformerException, IOException {
    createEmptyDocument();
    addDelimiter("DELIM");
    addHeaderDelimiter("HEADER-DELIM");
    Config config = new ConfigLoader().loadConfig(buildDocReader());
    assertTrue(config.matchesHeaderDelim("HEADER-DELIM"));
    assertFalse(config.matchesHeaderDelim("something else"));

  }

  @Test
  public void testPerLine() throws TransformerException, IOException {
    createEmptyDocument();
    addPerLineDelimiter();
    Config config = new ConfigLoader().loadConfig(buildDocReader());

    Delimiter delimiter = config.delimiters.get(0);
    Assert.assertNotNull(delimiter);
    assertTrue(delimiter.isPerLine);
    assertTrue(delimiter.matches("a line"));
    assertFalse(delimiter.matches(""));
  }

  @Test
  public void testRetainDelim() throws TransformerException, IOException {
    createEmptyDocument();
    Element delimiterNode = addDelimiter("a line");
    delimiterNode.setAttribute("retain", "true");
    Config config = new ConfigLoader().loadConfig(buildDocReader());

    Delimiter delimiter = config.delimiters.get(0);
    Assert.assertNotNull(delimiter);
    assertTrue(delimiter.isRetainDelim());
    assertTrue(delimiter.isRetainDelim);
  }

  @Test
  public void testDelimAtStart() throws TransformerException, IOException {
    createEmptyDocument();
    Element delimiterNode = addDelimiter("a line");
    delimiterNode.setAttribute("at-start", "true");
    Config config = new ConfigLoader().loadConfig(buildDocReader());

    Delimiter delimiter = config.delimiters.get(0);
    Assert.assertNotNull(delimiter);
    assertTrue(delimiter.isDelimAtStartOfRecord());

    // here we check if the retain delim  field is false
    // but the start of record is true, that the retain delim
    // operation will still return true
    assertTrue(delimiter.isRetainDelim());
    assertFalse(delimiter.isRetainDelim);
  }

  /**
   * Create a valid document that can handle headers and then confirm it with parsing
   */
  @Test
  public void testValidHeaderConfigOk() throws TransformerException, IOException {
    createEmptyDocument();
    addDelimiter("DELIM");
    addSearch("number", "(\\d*)");
    addHeaderValue("header", "a header {number}");
    Config config = new ConfigLoader().loadConfig(buildDocReader());

    Map<String, String> header = config.buildHeader("this is a header 999y hello");
    assertEquals("999", header.get("header"));
  }

  /**
   * Add a search to the config
   * @param id the id
   * @param regex the regex
   */
  private void addSearch(String id, String regex) {
    Element groupNode = findGroupNode("searches");
    createElementInGroup(groupNode, "search", id, regex);
  }

  /**
   * Add a value to the config
   * @param id the id
   * @param regex the regex
   */
  private void addValue(String id, String regex) {
    Element groupNode = findGroupNode("values");
    createElementInGroup(groupNode, "value", id, regex);
  }

  /**
   * Add a header value to the config
   * @param id the id
   * @param regex the regex
   */
  private void addHeaderValue(String id, String regex) {
    Element groupNode = findGroupNode("header");
    createElementInGroup(groupNode, "value", id, regex);
  }

  /**
   * Create and add an element in the group
   * @param groupNode the group node
   * @param elementName the new element's name
   * @param id the id
   * @param regex the regex
   */
  private void createElementInGroup(Element groupNode, String elementName, String id, String regex) {
    Element elementNode = document.createElement(elementName);
    elementNode.setAttribute("id", id);
    elementNode.setTextContent(regex);
    groupNode.appendChild(elementNode);
  }

  /**
   * Find a group under the config node - add it if it's not there
   * @param group the group name
   * @return the group element
   */
  private Element findGroupNode(String group) {
    Element configNode = findConfigNode();
    Element groupNode;
    NodeList groupNodes = configNode.getElementsByTagName(group);
    if (null == groupNodes || groupNodes.getLength() == 0) {
      groupNode = document.createElement(group);
      configNode.appendChild(groupNode);
    } else {
      groupNode = (Element) groupNodes.item(0);
    }
    return groupNode;
  }

  /**
   * Build a Reader for the document - this will contain the XML doc for the parser
   * @return the reader
   * @throws TransformerException
   */
  private Reader buildDocReader() throws TransformerException {
    // use the DOM writing tools to write the XML to this document
    StringWriter writer = new StringWriter();
    TransformerFactory tFactory = TransformerFactory.newInstance();
    Transformer transformer = tFactory.newTransformer();

    DOMSource source = new DOMSource(document);
    StreamResult result = new StreamResult(writer);
    transformer.transform(source, result);

    // return this as a reader
    return new StringReader(writer.toString());
  }

  /**
   * Create the empty document with just the config node in there
   * @return the config element
   */
  private Element createEmptyDocument() {
    Element element = document.createElement("config");
    document.appendChild(element);
    return element;
  }

  /**
   * Create and add a delimiter element to the config node
   * @return the empty delimiter
   */
  private Element addDelimiterElement() {
    Element element = findConfigNode();
    Element newChild = document.createElement("delim");
    element.appendChild(newChild);
    return newChild;
  }

  /**
   * Add a delimiter to the config node
   * @param delim the regex for the delim
   */
  private Element addDelimiter(String delim) {
    Element newChild = addDelimiterElement();
    newChild.setTextContent(delim);
    return newChild;
  }

  private void addPerLineDelimiter() {
    Element newChild = addDelimiterElement();
    newChild.setAttribute("per-line", "true");
  }

  private Element addHeaderDelimiter(String delim) {
    Element element = findConfigNode();
    Element newChild = document.createElement("header-delim");
    element.appendChild(newChild);
    newChild.setTextContent(delim);
    return element;
  }

  /**
   * Find the config node in the document
   * @return the config node
   */
  private Element findConfigNode() {
    String name = "config";
    NodeList nodes = document.getElementsByTagName(name);
    return (Element) nodes.item(0);
  }

}