/*
 * Copyright (C) 2008 Steve Ratcliffe
 * 
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 * 
 * Author: Steve Ratcliffe
 * Create date: 29-Nov-2008
 */
package uk.me.parabola.mkgmap.osmstyle;

import java.io.Reader;
import java.io.StringReader;
import java.util.List;
import java.util.Map;

import uk.me.parabola.mkgmap.osmstyle.actions.Action;
import uk.me.parabola.mkgmap.osmstyle.actions.ActionReader;
import uk.me.parabola.mkgmap.reader.osm.Element;
import uk.me.parabola.mkgmap.reader.osm.GeneralRelation;
import uk.me.parabola.mkgmap.reader.osm.Relation;
import uk.me.parabola.mkgmap.reader.osm.Rule;
import uk.me.parabola.mkgmap.reader.osm.TypeResult;
import uk.me.parabola.mkgmap.reader.osm.Way;
import uk.me.parabola.mkgmap.scan.SyntaxException;
import uk.me.parabola.mkgmap.scan.TokenScanner;

import org.hamcrest.core.StringContains;
import org.junit.Test;

import static org.junit.Assert.*;

/**
 * Test the possible actions that can appear in an action block.
 * These are run before any rule is finally matched.
 */
public class ActionReaderTest {

	@Test
	public void testSimpleSet() {
		List<Action> actions = readActionsFromString("{set park=yes}");
		assertEquals("one action", 1, actions.size());

		Element el = stdElementRun(actions);

		assertEquals("park overwritten", "yes", el.getTag("park"));
	}

	@Test
	public void testSimpleAdd() {
		List<Action> actions = readActionsFromString("{add park=yes}");
		assertEquals("one action", 1, actions.size());

		// add does not overwrite existing tags.
		Element el = stdElementRun(actions);
		assertEquals("park not overwritten", "no", el.getTag("park"));
	}

	@Test
	public void testRename() {
		List<Action> actions = readActionsFromString("{rename park landarea}");
		assertEquals("one action", 1, actions.size());

		Element el = stdElementRun(actions);
		assertNull("park should be gone", el.getTag("park"));
		assertEquals("park renamed", "no", el.getTag("landarea"));
	}

	/**
	 * Test with embedded comment, newlines, semicolon used as separator.
	 */
	@Test
	public void testFreeForm() {
		List<Action> actions = readActionsFromString(" { set web='world wide';" +
				"set \nribbon = 'yellow' \n# a comment } ");

		assertEquals("number of actions", 2, actions.size());
		Element el = stdElementRun(actions);
		assertEquals("park not overwritten", "no", el.getTag("park"));
		assertEquals("word with spaces", "world wide", el.getTag("web"));
		assertEquals("yellow ribbon", "yellow", el.getTag("ribbon"));
	}

	/**
	 * Test several commands in the block.  They should all be executed.
	 */
	@Test
	public void testMultipleCommands() {
		List<Action> actions = readActionsFromString(
				"{set park=yes; add fred=other;" +
						"set pooh=bear}");

		assertEquals("number of actions", 3, actions.size());

		Element el = stdElementRun(actions);

		assertEquals("park set to yes", "yes", el.getTag("park"));
		assertEquals("fred set", "other", el.getTag("fred"));
		assertEquals("pooh set", "bear", el.getTag("pooh"));
	}

	@Test(expected = SyntaxException.class)
	public void testInvalidCommand() {
		readActionsFromString("{bad }");
	}

	/**
	 * The name action set the element-name (not the 'name' tag).
	 * The first value to set it counts, later matches are ignored.
	 */
	@Test
	public void testName() {
		List<Action> actions = readActionsFromString("{name '${name} (${ref})' |" +
				"  '${ref}' | '${name}' ; }");
		Element el = makeElement();
		el.addTag("name", "Main St");
		Rule rule = new ActionRule(null, actions);
		rule.resolveType(el, TypeResult.NULL_RESULT);
		assertEquals("just name", "Main St", el.getName());
	}

	/**
	 * Test with two name actions.  This works just the same as having several
	 * name options on the same name command, in that it is still the
	 * first one to match that counts.
	 */
	@Test
	public void testDoubleName() {
		List<Action> actions = readActionsFromString("{name '${name} (${ref})' |" +
				"  '${ref}' | '${name}' ; " +
				" name 'fred';}");

		// Something that matches nothing in the first name command.
		Element el = makeElement();
		Rule rule = new ActionRule(null, actions);
		rule.resolveType(el, TypeResult.NULL_RESULT);
		assertEquals("no tags, second action matches", "fred", el.getName());

		el = makeElement();
		el.addTag("ref", "A1");
		rule.resolveType(el, TypeResult.NULL_RESULT);
		assertEquals("just a ref tag", "A1", el.getName());

		el = makeElement();
		el.addTag("ref", "A1");
		el.addTag("name", "Main St");
		rule.resolveType(el, TypeResult.NULL_RESULT);
		assertEquals("ref and name", "Main St (A1)", el.getName());
	}

	/**
	 * The apply action works on the members of relations.
	 */
	@Test
	public void testApplyAction() {
		List<Action> actions = readActionsFromString("{apply {" +
				"add route=bike;" +
				"set foo=bar; }" +
				"}\n");

		Relation rel = makeRelation();
		Rule rule = new ActionRule(null, actions);
		rule.resolveType(rel, TypeResult.NULL_RESULT);

		assertNull("Tag not set on relation", rel.getTag("route"));

		// Will be set on all members as there is no role filter.
		List<Map.Entry<String,Element>> elements = rel.getElements();
		Element el1 = elements.get(0).getValue();
		assertEquals("route tag added to first", "bike", el1.getTag("route"));
		assertEquals("foo tag set to first", "bar", el1.getTag("foo"));

		Element el2 = elements.get(1).getValue();
		assertEquals("route tag added to second", "bike", el2.getTag("route"));
		assertEquals("foo tag set to second", "bar", el2.getTag("foo"));
	}

	/**
	 * You can have a role filter, so that the actions are only applied
	 * to members with the given role.
	 */
	@Test
	public void testApplyWithRole() {
		List<Action> actions = readActionsFromString("{apply role=bar {" +
				"add route=bike;" +
				"set foo=bar; }}");

		Relation rel = makeRelation();
		Rule rule = new ActionRule(null, actions);
		rule.resolveType(rel, TypeResult.NULL_RESULT);

		List<Map.Entry<String,Element>> elements = rel.getElements();
		Element el1 = elements.get(0).getValue();
		assertEquals("route tag added to first", "bike", el1.getTag("route"));
		assertEquals("foo tag set to first", "bar", el1.getTag("foo"));

		// Wrong role, so not applied.
		Element el2 = elements.get(1).getValue();
		assertNull("route tag not added to second element (role=foo)", el2.getTag("route"));
		assertNull("foo tag not set in second element (role=foo)", el2.getTag("foo"));
	}

	/**
	 * When an apply statement runs, then substitutions on the value use
	 * the tags of the relation and not of the sub element.
	 */
	@Test
	public void testApplyWithSubst() {
		List<Action> actions = readActionsFromString("{apply {" +
				"add route='${route_no}';" +
				"}}");

		Relation rel = makeRelation();
		rel.addTag("route_no", "66");
		Element el1 = rel.getElements().get(0).getValue();
		el1.addTag("route_no", "42");

		Rule rule = new ActionRule(null, actions);
		rule.resolveType(rel, TypeResult.NULL_RESULT);
		assertEquals("route_no taken from relation tags", "66", el1.getTag("route"));
	}

	@Test
	public void testEmptyActionList() {
		List<Action> actions = readActionsFromString("{}");
		assertEquals("no actions found", 0, actions.size());		
	}

	@Test
	public void testAlternatives() {
		List<Action> actions = readActionsFromString(
				"{set fred = '${park}' | 'default value'}");

		Element el = makeElement();
		Rule rule = new ActionRule(null, actions);
		rule.resolveType(el, TypeResult.NULL_RESULT);
		assertEquals("first alternative", "no", el.getTag("fred"));
	}

	@Test
	public void testSecondAlternative() {
		List<Action> actions = readActionsFromString(
				"{set fred = '${notset}' | 'default value'}");

		Element el = makeElement();
		el.addTag("fred", "origvalue");
		Rule rule = new ActionRule(null, actions);
		rule.resolveType(el, TypeResult.NULL_RESULT);
		assertEquals("second alternative", "default value", el.getTag("fred"));
	}

	@Test
	public void testMultipleNoSeparators() {
		List<Action> actions = readActionsFromString("{" +
				"set park='${notset}' | yes " +
				"add fred=other " +
				"set pooh=bear}");

		assertEquals("number of actions", 3, actions.size());

		Element el = stdElementRun(actions);

		assertEquals("park set to yes", "yes", el.getTag("park"));
		assertEquals("fred set", "other", el.getTag("fred"));
		assertEquals("pooh set", "bear", el.getTag("pooh"));
	}

	@Test(expected = SyntaxException.class)
	public void testErrorShortSet() {
		readActionsFromString("{set park= }");
	}

	@Test(expected = SyntaxException.class)
	public void testMangledSet() {
		readActionsFromString("{set park=yes some other junk }");
	}

	@Test(expected = SyntaxException.class)
	public void testErrorMangledList() {
		readActionsFromString("{set park='${notset}' | }");
	}

	@Test
	public void testErrorExtraQuotedWord() {
		try {
			readActionsFromString("{set park=yes 'some' other junk }");
			assert false;  // should not get here
		} catch (SyntaxException e) {
			assertThat(e.getMessage(), new StringContains("quoted word found where command expected"));
		}
	}

	private Element stdElementRun(List<Action> actions) {
		Rule rule = new ActionRule(null, actions);
		Element el = makeElement();
		rule.resolveType(el, TypeResult.NULL_RESULT);
		return el;
	}

	/**
	 * Make a standard element for the tests.
	 */
	private Element makeElement() {
		Element el = new Way(0);
		el.addTag("park", "no");
		el.addTag("test", "1");
		return el;
	}

	private Relation makeRelation() {
		Relation rel = new GeneralRelation(23);
		rel.addElement("bar", makeElement());
		rel.addElement("foo", makeElement());
		return rel;
	}
	/**
	 * Read a action list from a string.
	 */
	private List<Action> readActionsFromString(String in) {
		Reader sr = new StringReader(in);
		TokenScanner ts = new TokenScanner("string", sr);
		ActionReader ar = new ActionReader(ts);
		return ar.readActions().getList();
	}
}
