/*
 *  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.tomcat.util.http;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;

import org.junit.Assert;
import org.junit.Test;

public class TestParameters {

    private static final Parameter SIMPLE =
        new Parameter("foo1", "bar1");
    private static final Parameter SIMPLE_MULTIPLE =
        new Parameter("foo2", "bar1", "bar2", "hello world", "?%@");
    private static final Parameter NO_VALUE =
        new Parameter("foo3");
    private static final Parameter EMPTY_VALUE =
        new Parameter("foo4", "");
    private static final Parameter EMPTY =
        new Parameter("");
    private static final Parameter UTF8 =
            new Parameter("\ufb6b\ufb6a\ufb72", "\uffee\uffeb\uffe2");

    @Test
    public void testProcessParametersByteArrayIntIntValid() {
        doTestProcessParametersByteArrayIntInt(-1, SIMPLE);
        doTestProcessParametersByteArrayIntInt(-1, SIMPLE_MULTIPLE);
        doTestProcessParametersByteArrayIntInt(-1, NO_VALUE);
        doTestProcessParametersByteArrayIntInt(-1, EMPTY_VALUE);
        doTestProcessParametersByteArrayIntInt(-1, EMPTY);
        doTestProcessParametersByteArrayIntInt(-1, UTF8);
        doTestProcessParametersByteArrayIntInt(-1,
                SIMPLE, SIMPLE_MULTIPLE, NO_VALUE, EMPTY_VALUE, EMPTY, UTF8);
        doTestProcessParametersByteArrayIntInt(-1,
                SIMPLE_MULTIPLE, NO_VALUE, EMPTY_VALUE, EMPTY, UTF8, SIMPLE);
        doTestProcessParametersByteArrayIntInt(-1,
                NO_VALUE, EMPTY_VALUE, EMPTY, UTF8, SIMPLE, SIMPLE_MULTIPLE);
        doTestProcessParametersByteArrayIntInt(-1,
                EMPTY_VALUE, EMPTY, UTF8, SIMPLE, SIMPLE_MULTIPLE, NO_VALUE);
        doTestProcessParametersByteArrayIntInt(-1,
                EMPTY, UTF8, SIMPLE, SIMPLE_MULTIPLE, NO_VALUE, EMPTY_VALUE);
        doTestProcessParametersByteArrayIntInt(-1,
                UTF8, SIMPLE, SIMPLE_MULTIPLE, NO_VALUE, EMPTY_VALUE, EMPTY);

        doTestProcessParametersByteArrayIntInt(4,
                SIMPLE, NO_VALUE, EMPTY_VALUE, UTF8);
    }

    @Test(expected = IllegalStateException.class)
    public void testProcessParametersByteArrayIntIntInvalid01() {
        doTestProcessParametersByteArrayIntInt(1,
                SIMPLE, NO_VALUE, EMPTY_VALUE, UTF8);
    }

    @Test(expected = IllegalStateException.class)
    public void testProcessParametersByteArrayIntIntInvalid02() {
        doTestProcessParametersByteArrayIntInt(2,
                SIMPLE, NO_VALUE, EMPTY_VALUE, UTF8);
    }

    @Test(expected = IllegalStateException.class)
    public void testProcessParametersByteArrayIntIntInvalid03() {
        doTestProcessParametersByteArrayIntInt(3,
                SIMPLE, NO_VALUE, EMPTY_VALUE, UTF8);
    }

    // Make sure the inner Parameter class behaves correctly
    @Test
    public void testInternal() {
        Assert.assertEquals("foo1=bar1", SIMPLE.toString());
        // Note: testing requires that ' ' is encoded as '+',
        // because that is what browsers will send us.
        Assert.assertEquals("foo2=bar1&foo2=bar2&foo2=hello+world&foo2=%3F%25%40",
                SIMPLE_MULTIPLE.toString());
        Assert.assertEquals("foo3", NO_VALUE.toString());
        Assert.assertEquals("foo4=", EMPTY_VALUE.toString());
    }

    private long doTestProcessParametersByteArrayIntInt(int limit,
            Parameter... parameters) {

        // Build the byte array
        StringBuilder input = new StringBuilder();
        boolean first = true;
        for (Parameter parameter : parameters) {
            if (first) {
                first = false;
            } else {
                input.append('&');
            }
            input.append(parameter.toString());
        }

        byte[] data = input.toString().getBytes();

        Parameters p = new Parameters();
        p.setCharset(StandardCharsets.UTF_8);
        p.setLimit(limit);

        long start = System.nanoTime();
        p.processParameters(data, 0, data.length);
        long end = System.nanoTime();

        if (limit == -1) {
            validateParameters(parameters, p);
        } else {
            Parameter[] limitParameters = new Parameter[limit];
            System.arraycopy(parameters, 0, limitParameters, 0, limit);
            validateParameters(limitParameters, p);
        }
        return end - start;
    }

    @Test
    public void testNonExistantParameter() {
        Parameters p = new Parameters();

        String value = p.getParameter("foo");
        Assert.assertNull(value);

        Enumeration<String> names = p.getParameterNames();
        Assert.assertFalse(names.hasMoreElements());

        String[] values = p.getParameterValues("foo");
        Assert.assertNull(values);
    }


    @Test
    public void testAddParameters() {
        Parameters p = new Parameters();

        // Empty at this point
        Enumeration<String> names = p.getParameterNames();
        Assert.assertFalse(names.hasMoreElements());
        String[] values = p.getParameterValues("foo");
        Assert.assertNull(values);

        // Add a parameter with two values
        p.addParameter("foo", "value1");
        p.addParameter("foo", "value2");

        names = p.getParameterNames();
        Assert.assertTrue(names.hasMoreElements());
        Assert.assertEquals("foo", names.nextElement());
        Assert.assertFalse(names.hasMoreElements());

        values = p.getParameterValues("foo");
        Assert.assertEquals(2, values.length);
        Assert.assertEquals("value1", values[0]);
        Assert.assertEquals("value2", values[1]);

        // Add two more values
        p.addParameter("foo", "value3");
        p.addParameter("foo", "value4");

        names = p.getParameterNames();
        Assert.assertTrue(names.hasMoreElements());
        Assert.assertEquals("foo", names.nextElement());
        Assert.assertFalse(names.hasMoreElements());

        values = p.getParameterValues("foo");
        Assert.assertEquals(4, values.length);
        Assert.assertEquals("value1", values[0]);
        Assert.assertEquals("value2", values[1]);
        Assert.assertEquals("value3", values[2]);
        Assert.assertEquals("value4", values[3]);
    }

    @Test
    public void testAddParametersLimit() {
        Parameters p = new Parameters();

        p.setLimit(2);

        // Empty at this point
        Enumeration<String> names = p.getParameterNames();
        Assert.assertFalse(names.hasMoreElements());
        String[] values = p.getParameterValues("foo1");
        Assert.assertNull(values);

        // Add a parameter
        p.addParameter("foo1", "value1");

        names = p.getParameterNames();
        Assert.assertTrue(names.hasMoreElements());
        Assert.assertEquals("foo1", names.nextElement());
        Assert.assertFalse(names.hasMoreElements());

        values = p.getParameterValues("foo1");
        Assert.assertEquals(1, values.length);
        Assert.assertEquals("value1", values[0]);

        // Add another parameter
        p.addParameter("foo2", "value2");

        names = p.getParameterNames();
        Assert.assertTrue(names.hasMoreElements());
        Assert.assertEquals("foo1", names.nextElement());
        Assert.assertEquals("foo2", names.nextElement());
        Assert.assertFalse(names.hasMoreElements());

        values = p.getParameterValues("foo1");
        Assert.assertEquals(1, values.length);
        Assert.assertEquals("value1", values[0]);

        values = p.getParameterValues("foo2");
        Assert.assertEquals(1, values.length);
        Assert.assertEquals("value2", values[0]);

        // Add another parameter
        IllegalStateException e = null;
        try {
            p.addParameter("foo3", "value3");
        } catch (IllegalStateException ise) {
            e = ise;
        }
        Assert.assertNotNull(e);

        // Check current parameters remain unaffected
        names = p.getParameterNames();
        Assert.assertTrue(names.hasMoreElements());
        Assert.assertEquals("foo1", names.nextElement());
        Assert.assertEquals("foo2", names.nextElement());
        Assert.assertFalse(names.hasMoreElements());

        values = p.getParameterValues("foo1");
        Assert.assertEquals(1, values.length);
        Assert.assertEquals("value1", values[0]);

        values = p.getParameterValues("foo2");
        Assert.assertEquals(1, values.length);
        Assert.assertEquals("value2", values[0]);

    }

    private void validateParameters(Parameter[] parameters, Parameters p) {
        Enumeration<String> names = p.getParameterNames();

        int i = 0;
        while (names.hasMoreElements()) {
            while (parameters[i].getName() == null) {
                i++;
            }

            String name = names.nextElement();
            String[] values = p.getParameterValues(name);

            boolean match = false;

            for (Parameter parameter : parameters) {
                if (name.equals(parameter.getName())) {
                    match = true;
                    if (parameter.values.length == 0) {
                        // Special case
                        Assert.assertArrayEquals(new String[] {""}, values);
                    } else {
                        Assert.assertArrayEquals(parameter.getValues(), values);
                    }
                    break;
                }
            }
            Assert.assertTrue(match);
        }
    }

    private static class Parameter {
        private final String name;
        private final String[] values;

        Parameter(String name, String... values) {
            this.name = name;
            this.values = values;
        }

        public String getName() {
            return name;
        }

        public String[] getValues() {
            return values;
        }

        @Override
        public String toString() {
            try {
                StringBuilder result = new StringBuilder();
                boolean first = true;
                if (values.length == 0) {
                    return URLEncoder.encode(name, "UTF-8");
                }
                for (String value : values) {
                    if (first) {
                        first = false;
                    } else {
                        result.append('&');
                    }
                    if (name != null) {
                        result.append(URLEncoder.encode(name, "UTF-8"));
                    }
                    if (value != null) {
                        result.append('=');
                        result.append(URLEncoder.encode(value, "UTF-8"));
                    }
                }
                return result.toString();
            } catch (UnsupportedEncodingException ex) {
                return ex.toString();
            }
        }
    }
}
