/*
 * Copyright 2002-2008 the original author or authors.
 *
 * 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 org.springframework.web.servlet.tags.form;

import java.beans.PropertyEditor;
import java.beans.PropertyEditorSupport;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.Tag;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import org.springframework.beans.TestBean;
import org.springframework.beans.propertyeditors.CustomCollectionEditor;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import org.springframework.web.servlet.support.BindStatus;
import org.springframework.web.servlet.tags.TransformTag;

/**
 * @author Rob Harrop
 * @author Juergen Hoeller
 */
public class SelectTagTests extends AbstractFormTagTests {

	private static final Locale LOCALE_AT = new Locale("de", "AT");
	private static final Locale LOCALE_NL = new Locale("nl", "NL");

	private SelectTag tag;

	private TestBean bean;


	protected void onSetUp() {
		this.tag = new SelectTag() {
			protected TagWriter createTagWriter() {
				return new TagWriter(getWriter());
			}
		};
		this.tag.setPageContext(getPageContext());
	}


	public void testEmptyItems() throws Exception {
		this.tag.setPath("country");
		this.tag.setItems(Collections.EMPTY_LIST);

		this.tag.setItemValue("isoCode");
		this.tag.setItemLabel("name");
		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);

		String output = getOutput();
		assertEquals("<select id=\"country\" name=\"country\"></select>", output);
	}

	public void testNullItems() throws Exception {
		this.tag.setPath("country");
		this.tag.setItems(null);

		this.tag.setItemValue("isoCode");
		this.tag.setItemLabel("name");
		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);

		String output = getOutput();
		assertEquals("<select id=\"country\" name=\"country\"></select>", output);
	}

	public void testWithList() throws Exception {
		this.tag.setPath("country");
		this.tag.setItems(Country.getCountries());
		assertList(true);
	}

	public void testWithResolvedList() throws Exception {
		this.tag.setPath("country");
		this.tag.setItems("${countries}");
		assertList(true);
	}

	public void testWithOtherValue() throws Exception {
		TestBean tb = getTestBean();
		tb.setCountry("AT");
		this.tag.setPath("country");
		this.tag.setItems(Country.getCountries());
		assertList(false);
	}

	public void testWithNullValue() throws Exception {
		TestBean tb = getTestBean();
		tb.setCountry(null);
		this.tag.setPath("country");
		this.tag.setItems(Country.getCountries());
		assertList(false);
	}

	public void testWithListAndNoLabel() throws Exception {
		this.tag.setPath("country");
		this.tag.setItems("${countries}");
		this.tag.setItemValue("isoCode");
		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);
		validateOutput(getOutput(), true);
	}

	public void testWithListAndTransformTag() throws Exception {
		this.tag.setPath("country");
		this.tag.setItems(Country.getCountries());
		assertList(true);

		TransformTag transformTag = new TransformTag();
		transformTag.setValue(Country.getCountries().get(0));
		transformTag.setVar("key");
		transformTag.setParent(this.tag);
		transformTag.setPageContext(getPageContext());
		transformTag.doStartTag();
		assertEquals("Austria(AT)", getPageContext().findAttribute("key"));
	}

	public void testWithListAndTransformTagAndEditor() throws Exception {
		this.tag.setPath("realCountry");
		this.tag.setItems("${countries}");
		BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(getTestBean(), "testBean");
		bindingResult.getPropertyAccessor().registerCustomEditor(Country.class, new PropertyEditorSupport() {
			public void setAsText(String text) throws IllegalArgumentException {
				setValue(new Country(text, ""));
			}
			public String getAsText() {
				return ((Country) getValue()).getName();
			}
		});
		getPageContext().getRequest().setAttribute(BindingResult.MODEL_KEY_PREFIX + "testBean", bindingResult);
		this.tag.doStartTag();

		TransformTag transformTag = new TransformTag();
		transformTag.setValue(Country.getCountries().get(0));
		transformTag.setVar("key");
		transformTag.setParent(this.tag);
		transformTag.setPageContext(getPageContext());
		transformTag.doStartTag();
		assertEquals("Austria", getPageContext().findAttribute("key"));
	}

	public void testWithMap() throws Exception {
		this.tag.setPath("sex");
		this.tag.setItems("${sexes}");
		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);
	}

	public void testWithInvalidList() throws Exception {
		this.tag.setPath("country");
		this.tag.setItems("${other}");
		this.tag.setItemValue("isoCode");
		try {
			this.tag.doStartTag();
			fail("Must not be able to use a non-Collection typed value as the value of 'items'");
		}
		catch (JspException expected) {
			String message = expected.getMessage();
			assertTrue(message.indexOf("items") > -1);
			assertTrue(message.indexOf("org.springframework.beans.TestBean") > -1);
		}
	}

	public void testWithNestedOptions() throws Exception {
		this.tag.setPath("country");
		int result = this.tag.doStartTag();
		assertEquals(Tag.EVAL_BODY_INCLUDE, result);

		BindStatus value = (BindStatus) getPageContext().getAttribute(SelectTag.LIST_VALUE_PAGE_ATTRIBUTE);
		assertEquals("Selected country not exposed in page context", "UK", value.getValue());

		result = this.tag.doEndTag();
		assertEquals(Tag.EVAL_PAGE, result);
		this.tag.doFinally();

		String output = getOutput();
		assertTrue(output.startsWith("<select "));
		assertTrue(output.endsWith("</select>"));
		assertContainsAttribute(output, "name", "country");
	}

	public void testWithStringArray() throws Exception {
		this.tag.setPath("name");
		this.tag.setItems(getNames());
		assertStringArray();
	}

	public void testWithResolvedStringArray() throws Exception {
		this.tag.setPath("name");
		this.tag.setItems("${names}");
		assertStringArray();
	}

	public void testWithIntegerArray() throws Exception {
		this.tag.setPath("someIntegerArray");
		Integer[] array = new Integer[50];
		for (int i = 0; i < array.length; i++) {
			array[i] = new Integer(i);
		}
		this.tag.setItems(array);
		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);

		String output = getOutput();
		output = "<doc>" + output + "</doc>";

		SAXReader reader = new SAXReader();
		Document document = reader.read(new StringReader(output));
		Element rootElement = document.getRootElement();
		assertEquals(2, rootElement.elements().size());

		Element selectElement = rootElement.element("select");
		assertEquals("select", selectElement.getName());
		assertEquals("someIntegerArray", selectElement.attribute("name").getValue());

		List children = selectElement.elements();
		assertEquals("Incorrect number of children", array.length, children.size());

		Element e = (Element) selectElement.selectSingleNode("option[text() = '12']");
		assertEquals("'12' node not selected", "selected", e.attribute("selected").getValue());

		e = (Element) selectElement.selectSingleNode("option[text() = '34']");
		assertEquals("'34' node not selected", "selected", e.attribute("selected").getValue());
	}

	public void testWithFloatCustom() throws Exception {
		PropertyEditor propertyEditor = new SimpleFloatEditor();
		BeanPropertyBindingResult errors = new BeanPropertyBindingResult(getTestBean(), COMMAND_NAME);
		errors.getPropertyAccessor().registerCustomEditor(Float.class, propertyEditor);
		exposeBindingResult(errors);

		this.tag.setPath("myFloat");

		Float[] array = new Float[] {
				new Float("12.30"), new Float("12.32"), new Float("12.34"), new Float("12.36"),
				new Float("12.38"), new Float("12.40"), new Float("12.42"), new Float("12.44"),
				new Float("12.46"), new Float("12.48")
		};

		this.tag.setItems(array);
		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);

		String output = getOutput();
		assertTrue(output.startsWith("<select "));
		assertTrue(output.endsWith("</select>"));

		SAXReader reader = new SAXReader();
		Document document = reader.read(new StringReader(output));
		Element rootElement = document.getRootElement();
		assertEquals("select", rootElement.getName());
		assertEquals("myFloat", rootElement.attribute("name").getValue());
		List children = rootElement.elements();
		assertEquals("Incorrect number of children", array.length, children.size());

		Element e = (Element) rootElement.selectSingleNode("option[text() = '12.34f']");
		assertEquals("'12.34' node not selected", "selected", e.attribute("selected").getValue());

		e = (Element) rootElement.selectSingleNode("option[text() = '12.32f']");
		assertNull("'12.32' node incorrectly selected", e.attribute("selected"));
	}

	public void testWithMultiList() throws Exception {
		List list = new ArrayList();
		list.add(Country.COUNTRY_UK);
		list.add(Country.COUNTRY_AT);
		this.bean.setSomeList(list);

		this.tag.setPath("someList");
		this.tag.setItems("${countries}");
		this.tag.setItemValue("isoCode");
		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);

		String output = getOutput();
		output = "<doc>" + output + "</doc>";

		SAXReader reader = new SAXReader();
		Document document = reader.read(new StringReader(output));
		Element rootElement = document.getRootElement();
		assertEquals(2, rootElement.elements().size());

		Element selectElement = rootElement.element("select");
		assertEquals("select", selectElement.getName());
		assertEquals("someList", selectElement.attribute("name").getValue());

		List children = selectElement.elements();
		assertEquals("Incorrect number of children", 4, children.size());

		Element e = (Element) selectElement.selectSingleNode("option[@value = 'UK']");
		assertEquals("UK node not selected", "selected", e.attribute("selected").getValue());

		e = (Element) selectElement.selectSingleNode("option[@value = 'AT']");
		assertEquals("AT node not selected", "selected", e.attribute("selected").getValue());
	}

	public void testWithMultiListAndCustomEditor() throws Exception {
		List list = new ArrayList();
		list.add(Country.COUNTRY_UK);
		list.add(Country.COUNTRY_AT);
		this.bean.setSomeList(list);

		BeanPropertyBindingResult errors = new BeanPropertyBindingResult(this.bean, COMMAND_NAME);
		errors.getPropertyAccessor().registerCustomEditor(List.class, new CustomCollectionEditor(LinkedList.class) {
			public String getAsText() {
				return getValue().toString();
			}
		});
		exposeBindingResult(errors);

		this.tag.setPath("someList");
		this.tag.setItems("${countries}");
		this.tag.setItemValue("isoCode");
		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);

		String output = getOutput();
		output = "<doc>" + output + "</doc>";

		SAXReader reader = new SAXReader();
		Document document = reader.read(new StringReader(output));
		Element rootElement = document.getRootElement();
		assertEquals(2, rootElement.elements().size());

		Element selectElement = rootElement.element("select");
		assertEquals("select", selectElement.getName());
		assertEquals("someList", selectElement.attribute("name").getValue());

		List children = selectElement.elements();
		assertEquals("Incorrect number of children", 4, children.size());

		Element e = (Element) selectElement.selectSingleNode("option[@value = 'UK']");
		assertEquals("UK node not selected", "selected", e.attribute("selected").getValue());

		e = (Element) selectElement.selectSingleNode("option[@value = 'AT']");
		assertEquals("AT node not selected", "selected", e.attribute("selected").getValue());
	}

	public void testWithMultiMap() throws Exception {
		Map someMap = new HashMap();
		someMap.put("M", "Male");
		someMap.put("F", "Female");
		this.bean.setSomeMap(someMap);

		this.tag.setPath("someMap");
		this.tag.setItems("${sexes}");

		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);

		String output = getOutput();
		output = "<doc>" + output + "</doc>";

		SAXReader reader = new SAXReader();
		Document document = reader.read(new StringReader(output));
		Element rootElement = document.getRootElement();
		assertEquals(2, rootElement.elements().size());

		Element selectElement = rootElement.element("select");
		assertEquals("select", selectElement.getName());
		assertEquals("someMap", selectElement.attribute("name").getValue());

		List children = selectElement.elements();
		assertEquals("Incorrect number of children", 2, children.size());

		Element e = (Element) selectElement.selectSingleNode("option[@value = 'M']");
		assertEquals("M node not selected", "selected", e.attribute("selected").getValue());

		e = (Element) selectElement.selectSingleNode("option[@value = 'F']");
		assertEquals("F node not selected", "selected", e.attribute("selected").getValue());
	}

	/**
	 * Tests new support added as a result of <a
	 * href="http://opensource.atlassian.com/projects/spring/browse/SPR-2660"
	 * target="_blank">SPR-2660</a>.
	 * <p>
	 * Specifically, if the <code>items</code> attribute is supplied a
	 * {@link Map}, and <code>itemValue</code> and <code>itemLabel</code>
	 * are supplied non-null values, then:
	 * </p>
	 * <ul>
	 * <li><code>itemValue</code> will be used as the property name of the
	 * map's <em>key</em>, and</li>
	 * <li><code>itemLabel</code> will be used as the property name of the
	 * map's <em>value</em>.</li>
	 * </ul>
	 */
	public void testWithMultiMapWithItemValueAndItemLabel() throws Exception {

		// Save original default locale.
		final Locale defaultLocale = Locale.getDefault();
		// Use a locale that doesn't result in the generation of HTML entities
		// (e.g., not German, where ä becomes &auml;)
		Locale.setDefault(Locale.US);

		try {
			final Country austria = Country.COUNTRY_AT;
			final Country usa = Country.COUNTRY_US;
			final Map someMap = new HashMap();
			someMap.put(austria, LOCALE_AT);
			someMap.put(usa, Locale.US);
			this.bean.setSomeMap(someMap);

			this.tag.setPath("someMap"); // see: TestBean
			this.tag.setItems("${countryToLocaleMap}"); // see: extendRequest()
			this.tag.setItemValue("isoCode"); // Map key: Country
			this.tag.setItemLabel("displayLanguage"); // Map value: Locale

			BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(getTestBean(), COMMAND_NAME);
			bindingResult.getPropertyAccessor().registerCustomEditor(Country.class, new PropertyEditorSupport() {

				public void setAsText(final String text) throws IllegalArgumentException {
					setValue(Country.getCountryWithIsoCode(text));
				}

				public String getAsText() {
					return ((Country) getValue()).getIsoCode();
				}
			});
			exposeBindingResult(bindingResult);

			int result = this.tag.doStartTag();
			assertEquals(Tag.SKIP_BODY, result);

			String output = getOutput();
			output = "<doc>" + output + "</doc>";

			SAXReader reader = new SAXReader();
			Document document = reader.read(new StringReader(output));
			Element rootElement = document.getRootElement();
			assertEquals(2, rootElement.elements().size());

			Element selectElement = rootElement.element("select");
			assertEquals("select", selectElement.getName());
			assertEquals("someMap", selectElement.attribute("name").getValue());

			List children = selectElement.elements();
			assertEquals("Incorrect number of children", 3, children.size());

			Element e;
			e = (Element) selectElement.selectSingleNode("option[@value = '" + austria.getIsoCode() + "']");
			assertNotNull("Option node not found with Country ISO code value [" + austria.getIsoCode() + "].", e);
			assertEquals("AT node not selected.", "selected", e.attribute("selected").getValue());
			assertEquals("AT Locale displayLanguage property not used for option label.",
					LOCALE_AT.getDisplayLanguage(), e.getData());

			e = (Element) selectElement.selectSingleNode("option[@value = '" + usa.getIsoCode() + "']");
			assertNotNull("Option node not found with Country ISO code value [" + usa.getIsoCode() + "].", e);
			assertEquals("US node not selected.", "selected", e.attribute("selected").getValue());
			assertEquals("US Locale displayLanguage property not used for option label.",
					Locale.US.getDisplayLanguage(), e.getData());

		}
		finally {
			// Restore original default locale.
			Locale.setDefault(defaultLocale);
		}
	}

	public void testMultiWithEmptyCollection() throws Exception {
		this.bean.setSomeList(new ArrayList());

		this.tag.setPath("someList");
		this.tag.setItems("${countries}");
		this.tag.setItemValue("isoCode");
		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);

		String output = getOutput();
		output = "<doc>" + output + "</doc>";

		SAXReader reader = new SAXReader();
		Document document = reader.read(new StringReader(output));
		Element rootElement = document.getRootElement();
		assertEquals(2, rootElement.elements().size());

		Element selectElement = rootElement.element("select");
		assertEquals("select", selectElement.getName());
		assertEquals("someList", selectElement.attribute("name").getValue());
		assertEquals("multiple", selectElement.attribute("multiple").getValue());

		List children = selectElement.elements();
		assertEquals("Incorrect number of children", 4, children.size());

		Element inputElement = rootElement.element("input");
		assertNotNull(inputElement);
	}


	private void assertStringArray() throws JspException, DocumentException {
		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);

		String output = getOutput();
		assertTrue(output.startsWith("<select "));
		assertTrue(output.endsWith("</select>"));

		SAXReader reader = new SAXReader();
		Document document = reader.read(new StringReader(output));
		Element rootElement = document.getRootElement();
		assertEquals("select", rootElement.getName());
		assertEquals("name", rootElement.attribute("name").getValue());

		List children = rootElement.elements();
		assertEquals("Incorrect number of children", 4, children.size());

		Element e = (Element) rootElement.selectSingleNode("option[text() = 'Rob']");
		assertEquals("Rob node not selected", "selected", e.attribute("selected").getValue());
	}

	private Map getCountryToLocaleMap() {
		Map map = new TreeMap(new Comparator() {
			public int compare(Object o1, Object o2) {
				return ((Country)o1).getName().compareTo(((Country)o2).getName());
			}
		});
		map.put(Country.COUNTRY_AT, LOCALE_AT);
		map.put(Country.COUNTRY_NL, LOCALE_NL);
		map.put(Country.COUNTRY_US, Locale.US);
		return map;
	}

	private String[] getNames() {
		return new String[]{"Rod", "Rob", "Juergen", "Adrian"};
	}

	private Map getSexes() {
		Map sexes = new HashMap();
		sexes.put("F", "Female");
		sexes.put("M", "Male");
		return sexes;
	}

	protected void extendRequest(MockHttpServletRequest request) {
		super.extendRequest(request);
		request.setAttribute("countries", Country.getCountries());
		request.setAttribute("countryToLocaleMap", getCountryToLocaleMap());
		request.setAttribute("sexes", getSexes());
		request.setAttribute("other", new TestBean());
		request.setAttribute("names", getNames());
	}

	private void assertList(boolean selected) throws JspException, DocumentException {
		this.tag.setItemValue("isoCode");
		this.tag.setItemLabel("name");
		this.tag.setSize("5");
		int result = this.tag.doStartTag();
		assertEquals(Tag.SKIP_BODY, result);

		String output = getOutput();
		validateOutput(output, selected);
		assertContainsAttribute(output, "size", "5");
	}

	private void validateOutput(String output, boolean selected) throws DocumentException {
		SAXReader reader = new SAXReader();
		Document document = reader.read(new StringReader(output));
		Element rootElement = document.getRootElement();
		assertEquals("select", rootElement.getName());
		assertEquals("country", rootElement.attribute("name").getValue());

		List children = rootElement.elements();
		assertEquals("Incorrect number of children", 4, children.size());

		Element e = (Element) rootElement.selectSingleNode("option[@value = 'UK']");
		Attribute selectedAttr = e.attribute("selected");
		if (selected) {
			assertTrue(selectedAttr != null && "selected".equals(selectedAttr.getValue()));
		}
		else {
			assertNull(selectedAttr);
		}
	}

	protected TestBean createTestBean() {
		this.bean = new TestBeanWithRealCountry();
		this.bean.setName("Rob");
		this.bean.setCountry("UK");
		this.bean.setSex("M");
		this.bean.setMyFloat(new Float("12.34"));
		this.bean.setSomeIntegerArray(new Integer[]{new Integer(12), new Integer(34)});
		return this.bean;
	}

	private TestBean getTestBean() {
		return (TestBean) getPageContext().getRequest().getAttribute(COMMAND_NAME);
	}

}
