/*
 * 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.catalina.servlets;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import jakarta.servlet.http.HttpServletResponse;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;

import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.SimpleHttpClient;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.http.FastHttpDateFormat;

/**
 * This test case is used to verify RFC 9110 Section 13. Conditional Requests.
 */
@RunWith(Parameterized.class)
public class TestDefaultServletRfc9110Section13 extends TomcatBaseTest {

    @Parameter(0)
    public boolean useStrongETags;
    @Parameter(1)
    public Task task;
    @Parameter(2)
    public EtagPrecondition ifMatchPrecondition;
    @Parameter(3)
    public DatePrecondition ifUnmodifiedSincePrecondition;
    @Parameter(4)
    public EtagPrecondition ifNoneMatchPrecondition;
    @Parameter(5)
    public DatePrecondition ifModifiedSincePrecondition;
    @Parameter(6)
    public EtagPrecondition ifRangeEtagPrecondition;
    @Parameter(7)
    public DatePrecondition ifRangeDatePrecondition;
    @Parameter(8)
    public boolean addRangeHeader;
    @Parameter(9)
    public Integer scExpected;

    @Parameterized.Parameters(name = "{index} resource-strong [{0}], matchHeader [{1}]")
    public static Collection<Object[]> parameters() {
        List<Object[]> parameterSets = new ArrayList<>();
        for (Boolean useStrongEtag : booleans) {
            for (Task task : Arrays.asList(Task.HEAD_INDEX_HTML, Task.GET_INDEX_HTML, Task.POST_INDEX_HTML)) {
                // RFC 9110, Section 13.2.2, Step 1, HEAD: If-Match with and without If-Unmodified-Since
                for (DatePrecondition dateCondition : DatePrecondition.values()) {
                    parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.ALL, dateCondition, null,
                            null, null, null, Boolean.FALSE, SC_200 });
                    parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.EXACTLY, dateCondition, null,
                            null, null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_200 : SC_412 });
                    parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.IN, dateCondition, null,
                            null, null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_200 : SC_412 });
                    parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.NOT_IN, dateCondition, null,
                            null, null, null, Boolean.FALSE, SC_412 });
                    parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.INVALID, dateCondition, null,
                            null, null, null, Boolean.FALSE, SC_400 });
                    parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
                            dateCondition, null, null, null, null, Boolean.FALSE, SC_400 });
                }

                // RFC 9110, Section 13.2.2, Step 2, HEAD: If-Unmodified-Since only
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.EQ, null, null, null, null,
                        Boolean.FALSE, SC_200 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.LT, null, null, null, null,
                        Boolean.FALSE, SC_412 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.GT, null, null, null, null,
                        Boolean.FALSE, SC_200 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.MULTI_IN, null, null, null,
                        null, Boolean.FALSE, SC_200 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.MULTI_IN_REV, null, null,
                        null, null, Boolean.FALSE, SC_200 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.INVALID, null, null, null,
                        null, Boolean.FALSE, SC_200 });

                // Ensure If-Unmodified-Since takes precedence over If-Modified-Since
                // If-Unmodified-Since only
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.LT, null, null, null, null,
                        Boolean.FALSE, SC_412 });
                // If-Modified-Since only
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.GT, null, null,
                        Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_200 : SC_304 });
                // Both
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.LT, null,
                        DatePrecondition.GT, null, null, Boolean.FALSE, SC_412 });

                // RFC 9110, Section 13.2.2, Step 3, HEAD: If-None-Match with and without If-Modified-Since
                for (DatePrecondition dateCondition : DatePrecondition.values()) {
                    parameterSets
                            .add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.ALL, dateCondition,
                                    null, null, Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_412 : SC_304 });
                    parameterSets.add(
                            new Object[] { useStrongEtag, task, null, null, EtagPrecondition.EXACTLY, dateCondition,
                                    null, null, Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_412 : SC_304 });
                    parameterSets
                            .add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.IN, dateCondition,
                                    null, null, Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_412 : SC_304 });
                    parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.NOT_IN,
                            dateCondition, null, null, Boolean.FALSE, SC_200 });
                    parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.INVALID,
                            dateCondition, null, null, Boolean.FALSE, SC_400 });
                    parameterSets.add(
                            new Object[] { useStrongEtag, task, null, null, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
                                    dateCondition, null, null, Boolean.FALSE, SC_400 });
                }

                // RFC 9110, Section 13.2.2, Step 4, HEAD: If-Modified-Since only
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.EQ, null, null,
                        Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_200 : SC_304 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.LT, null, null,
                        Boolean.FALSE, SC_200 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.GT, null, null,
                        Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_200 : SC_304 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.MULTI_IN, null,
                        null, Boolean.FALSE, SC_200 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.MULTI_IN_REV,
                        null, null, Boolean.FALSE, SC_200 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.INVALID, null,
                        null, Boolean.FALSE, SC_200 });
            }

            for (Task task : Arrays.asList(Task.HEAD_404_HTML, Task.GET_404_HTML, Task.POST_404_HTML)) {
                // RFC 9110, Section 13.2.2, Step 1, HEAD: If-Match with and without If-Unmodified-Since
                for (DatePrecondition dateCondition : DatePrecondition.values()) {
                    parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.ALL, dateCondition, null,
                            null, null, null, Boolean.FALSE, SC_412 });
                    parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.IN, dateCondition, null,
                            null, null, null, Boolean.FALSE, SC_412 });
                    parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.NOT_IN, dateCondition, null,
                            null, null, null, Boolean.FALSE, SC_412 });
                    parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.INVALID, dateCondition, null,
                            null, null, null, Boolean.FALSE, SC_400 });
                    parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
                            dateCondition, null, null, null, null, Boolean.FALSE, SC_400 });
                }

                // RFC 9110, Section 13.2.2, Step 2, HEAD: If-Unmodified-Since only
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.EQ, null, null, null, null,
                        Boolean.FALSE, SC_404 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.LT, null, null, null, null,
                        Boolean.FALSE, SC_404 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.GT, null, null, null, null,
                        Boolean.FALSE, SC_404 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.MULTI_IN, null, null, null,
                        null, Boolean.FALSE, SC_404 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.MULTI_IN_REV, null, null,
                        null, null, Boolean.FALSE, SC_404 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.INVALID, null, null, null,
                        null, Boolean.FALSE, SC_404 });

                // RFC 9110, Section 13.2.2, Step 3, HEAD: If-None-Match with and without If-Modified-Since
                for (DatePrecondition dateCondition : DatePrecondition.values()) {
                    parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.ALL,
                            dateCondition, null, null, Boolean.FALSE, SC_404 });
                    parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.IN,
                            dateCondition, null, null, Boolean.FALSE, SC_404 });
                    parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.NOT_IN,
                            dateCondition, null, null, Boolean.FALSE, SC_404 });
                    parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.INVALID,
                            dateCondition, null, null, Boolean.FALSE, SC_400 });
                    parameterSets.add(
                            new Object[] { useStrongEtag, task, null, null, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
                                    dateCondition, null, null, Boolean.FALSE, SC_400 });
                }

                // RFC 9110, Section 13.2.2, Step 4, HEAD: If-Modified-Since only
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.EQ, null, null,
                        Boolean.FALSE, SC_404 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.LT, null, null,
                        Boolean.FALSE, SC_404 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.GT, null, null,
                        Boolean.FALSE, SC_404 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.MULTI_IN, null,
                        null, Boolean.FALSE, SC_404 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.MULTI_IN_REV,
                        null, null, Boolean.FALSE, SC_404 });
                parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.INVALID, null,
                        null, Boolean.FALSE, SC_404 });
            }

            // RFC 9110, Section 13.2.2, Step 5, GET: If-Range only
            // entity-tag
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.ALL, null, Boolean.TRUE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.EXACTLY, null, Boolean.TRUE, useStrongEtag.booleanValue() ? SC_206 : SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.IN, null, Boolean.TRUE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.NOT_IN, null, Boolean.TRUE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.INVALID, null, Boolean.TRUE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.INVALID_ALL_PLUS_OTHER, null, Boolean.TRUE, SC_400 });
            // HTTP-date
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.EQ, Boolean.TRUE, SC_206 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.LT, Boolean.TRUE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.GT, Boolean.TRUE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.MULTI_IN, Boolean.TRUE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.MULTI_IN_REV, Boolean.TRUE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.INVALID, Boolean.TRUE, SC_400 });
            // Range header without If-Range
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, EtagPrecondition.ALL, null, null, null,
                    null, null, Boolean.TRUE, SC_206 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, EtagPrecondition.EXACTLY, null, null,
                    null, null, null, Boolean.TRUE, useStrongEtag.booleanValue() ? SC_206 : SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, EtagPrecondition.IN, null, null, null,
                    null, null, Boolean.TRUE, useStrongEtag.booleanValue() ? SC_206 : SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, EtagPrecondition.NOT_IN, null, null,
                    null, null, null, Boolean.TRUE, SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, EtagPrecondition.INVALID, null, null,
                    null, null, null, Boolean.TRUE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML,
                    EtagPrecondition.INVALID_ALL_PLUS_OTHER, null, null, null, null, null, Boolean.TRUE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.EQ, null, null,
                    null, null, Boolean.TRUE, SC_206 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.LT, null, null,
                    null, null, Boolean.TRUE, SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.GT, null, null,
                    null, null, Boolean.TRUE, SC_206 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.MULTI_IN, null,
                    null, null, null, Boolean.TRUE, SC_206 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.MULTI_IN_REV,
                    null, null, null, null, Boolean.TRUE, SC_206 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.INVALID, null,
                    null, null, null, Boolean.TRUE, SC_206 });
            // If-Range header without Range
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.ALL, null, Boolean.FALSE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.EXACTLY, null, Boolean.FALSE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.IN, null, Boolean.FALSE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.NOT_IN, null, Boolean.FALSE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.INVALID, null, Boolean.FALSE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
                    EtagPrecondition.INVALID_ALL_PLUS_OTHER, null, Boolean.FALSE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.EQ, Boolean.FALSE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.LT, Boolean.FALSE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.GT, Boolean.FALSE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.MULTI_IN, Boolean.FALSE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.MULTI_IN_REV, Boolean.FALSE, SC_200 });
            parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
                    DatePrecondition.INVALID, Boolean.FALSE, SC_200 });

            // PUT tests
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, null, null, null, null, null, null,
                    Boolean.FALSE, SC_204 });
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.ALL, null, null, null,
                    null, null, Boolean.FALSE, SC_204 });
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.EXACTLY, null, null,
                    null, null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_204 : SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.IN, null, null, null,
                    null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_204 : SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.NOT_IN, null, null,
                    null, null, null, Boolean.FALSE, SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.INVALID, null, null,
                    null, null, null, Boolean.FALSE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
                    null, null, null, null, null, Boolean.FALSE, SC_400 });

            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, null, null, null, null, null, null,
                    Boolean.FALSE, SC_201 });
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, EtagPrecondition.ALL, null, null, null,
                    null, null, Boolean.FALSE, SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, EtagPrecondition.IN, null, null, null,
                    null, null, Boolean.FALSE, SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, EtagPrecondition.NOT_IN, null, null, null,
                    null, null, Boolean.FALSE, SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, EtagPrecondition.INVALID, null, null,
                    null, null, null, Boolean.FALSE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
                    null, null, null, null, null, Boolean.FALSE, SC_400 });

            // DELETE TESTS
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, null, null, null, null, null, null,
                    Boolean.FALSE, SC_204 });
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, EtagPrecondition.ALL, null, null,
                    null, null, null, Boolean.FALSE, SC_204 });
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, EtagPrecondition.EXACTLY, null, null,
                    null, null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_204 : SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, EtagPrecondition.IN, null, null,
                    null, null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_204 : SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, EtagPrecondition.NOT_IN, null, null,
                    null, null, null, Boolean.FALSE, SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, EtagPrecondition.INVALID, null, null,
                    null, null, null, Boolean.FALSE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT,
                    EtagPrecondition.INVALID_ALL_PLUS_OTHER, null, null, null, null, null, Boolean.FALSE, SC_400 });

            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT, null, null, null, null, null,
                    null, Boolean.FALSE, SC_404 });
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT, EtagPrecondition.ALL, null, null,
                    null, null, null, Boolean.FALSE, SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT, EtagPrecondition.IN, null, null,
                    null, null, null, Boolean.FALSE, SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT, EtagPrecondition.NOT_IN, null,
                    null, null, null, null, Boolean.FALSE, SC_412 });
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT, EtagPrecondition.INVALID, null,
                    null, null, null, null, Boolean.FALSE, SC_400 });
            parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT,
                    EtagPrecondition.INVALID_ALL_PLUS_OTHER, null, null, null, null, null, Boolean.FALSE, SC_400 });
        }

        return parameterSets;
    }


    private static Integer SC_200 = Integer.valueOf(HttpServletResponse.SC_OK);
    private static Integer SC_201 = Integer.valueOf(HttpServletResponse.SC_CREATED);
    private static Integer SC_204 = Integer.valueOf(HttpServletResponse.SC_NO_CONTENT);
    private static Integer SC_206 = Integer.valueOf(HttpServletResponse.SC_PARTIAL_CONTENT);
    private static Integer SC_304 = Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED);
    private static Integer SC_400 = Integer.valueOf(HttpServletResponse.SC_BAD_REQUEST);
    private static Integer SC_404 = Integer.valueOf(HttpServletResponse.SC_NOT_FOUND);
    private static Integer SC_412 = Integer.valueOf(HttpServletResponse.SC_PRECONDITION_FAILED);


    private enum HTTP_METHOD {
        GET,
        PUT,
        DELETE,
        POST,
        HEAD
    }


    private enum Task {
        HEAD_INDEX_HTML(HTTP_METHOD.HEAD, "/index.html"),
        HEAD_404_HTML(HTTP_METHOD.HEAD, "/sc_404.html"),

        GET_INDEX_HTML(HTTP_METHOD.GET, "/index.html"),
        GET_404_HTML(HTTP_METHOD.GET, "/sc_404.html"),

        POST_INDEX_HTML(HTTP_METHOD.POST, "/index.html"),
        POST_404_HTML(HTTP_METHOD.POST, "/sc_404.html"),

        PUT_EXIST_TXT(HTTP_METHOD.PUT, "/put_exist.txt"),
        PUT_NEW_TXT(HTTP_METHOD.PUT, "/put_new.txt"),

        DELETE_EXIST_TXT(HTTP_METHOD.DELETE, "/delete_exist.txt"),
        DELETE_NOT_EXIST_TXT(HTTP_METHOD.DELETE, "/delete_404.txt");

        HTTP_METHOD m;
        String uri;

        Task(HTTP_METHOD m, String uri) {
            this.m = m;
            this.uri = uri;
        }

        @Override
        public String toString() {
            return m.name() + " " + uri;
        }
    }


    private enum EtagPrecondition {
        EXACTLY,
        IN,
        ALL,
        NOT_IN,
        INVALID,
        INVALID_ALL_PLUS_OTHER
    }


    private enum DatePrecondition {
        /**
         * Condition header value of http date is equivalent to actual resource lastModified date
         */
        EQ,
        /**
         * Condition header value of http date is greater(later) than actual resource lastModified date
         */
        GT,
        /**
         * Condition header value of http date is less(earlier) than actual resource lastModified date
         */
        LT,
        MULTI_IN,
        MULTI_IN_REV,
        /**
         * not a valid HTTP-date
         */
        INVALID,
        /**
         * None.
         */
        NONE;
    }


    protected void genETagPrecondition(String strongETag, String weakETag, EtagPrecondition condition,
            String headerName, Map<String,List<String>> requestHeaders) {
        if (condition == null) {
            return;
        }
        List<String> headerValues = new ArrayList<>();
        switch (condition) {
            case ALL:
                headerValues.add("*");
                break;
            case EXACTLY:
                if (strongETag != null) {
                    headerValues.add(strongETag);
                } else {
                    // Should not happen
                    throw new IllegalArgumentException("strong etag not found!");
                }
                break;
            case IN:
                headerValues.add("\"1a2b3c4d\"");
                headerValues.add((weakETag != null ? weakETag + "," : "") +
                        (strongETag != null ? strongETag + "," : "") + "W/\"*\"");
                headerValues.add("\"abcdefg\"");
                break;
            case NOT_IN:
                headerValues.add("\"1a2b3c4d\"");
                if (weakETag != null && weakETag.length() > 8) {
                    headerValues.add(weakETag.substring(0, 3) + "XXXXX" + weakETag.substring(8));
                }
                if (strongETag != null && strongETag.length() > 6) {
                    headerValues.add(strongETag.substring(0, 1) + "XXXXX" + strongETag.substring(6));
                }
                headerValues.add("\"abcdefg\"");
                break;
            case INVALID:
                headerValues.add("invalid-no-quotes");
                break;
            case INVALID_ALL_PLUS_OTHER:
                headerValues.add("*");
                headerValues.add("W/\"1abcd\"");
                break;
        }
        if (!headerValues.isEmpty()) {
            requestHeaders.put(headerName, headerValues);
        }
    }


    protected void genDatePrecondition(long lastModifiedTimestamp, DatePrecondition condition, String headerName,
            Map<String,List<String>> requestHeaders) {
        if (condition == null || lastModifiedTimestamp <= 0) {
            return;
        }
        List<String> headerValues = new ArrayList<>();
        switch (condition) {
            case EQ:
                headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
                break;
            case GT:
                headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
                break;
            case LT:
                headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
                break;
            case MULTI_IN:
                headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
                headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
                headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
                break;
            case MULTI_IN_REV:
                headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
                headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
                headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
                break;
            case INVALID:
                headerValues.add("2024.12.09 GMT");
                break;
            case NONE:
                // NO-OP
                break;
        }
        if (!headerValues.isEmpty()) {
            requestHeaders.put(headerName, headerValues);
        }
    }

    protected void addPreconditionHeaders(Map<String,List<String>> requestHeaders, String resourceETag,
            long lastModified) {

        String weakETag = resourceETag;
        String strongETag = resourceETag;
        if (resourceETag != null) {
            if (resourceETag.startsWith("W/")) {
                strongETag = resourceETag.substring(2);
            } else {
                weakETag = "W/" + resourceETag;
            }
        }

        genETagPrecondition(strongETag, weakETag, ifMatchPrecondition, "If-Match", requestHeaders);
        genDatePrecondition(lastModified, ifUnmodifiedSincePrecondition, "If-Unmodified-Since", requestHeaders);
        genETagPrecondition(strongETag, weakETag, ifNoneMatchPrecondition, "If-None-Match", requestHeaders);
        genDatePrecondition(lastModified, ifModifiedSincePrecondition, "If-Modified-Since", requestHeaders);
        genETagPrecondition(strongETag, weakETag, ifRangeEtagPrecondition, "If-Range", requestHeaders);
        genDatePrecondition(lastModified, ifRangeDatePrecondition, "If-Range", requestHeaders);
    }

    private File tempDocBase = null;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        tempDocBase = Files.createTempDirectory(getTemporaryDirectory().toPath(), "conditional").toFile();
        long lastModified = FastHttpDateFormat.parseDate("Fri, 06 Dec 2024 00:00:00 GMT");
        Files.write(Path.of(tempDocBase.getAbsolutePath(), "index.html"), "<html><body>Index</body></html>".getBytes(),
                StandardOpenOption.CREATE);
        Path.of(tempDocBase.getAbsolutePath(), "index.html").toFile().setLastModified(lastModified);

        if (task.m.equals(HTTP_METHOD.PUT)) {
            Files.write(Path.of(tempDocBase.getAbsolutePath(), "put_exist.txt"), "put_exist_v0".getBytes(),
                    StandardOpenOption.CREATE);
            Path.of(tempDocBase.getAbsolutePath(), "put_exist.txt").toFile().setLastModified(lastModified);
        }

        if (task.m.equals(HTTP_METHOD.DELETE)) {
            Files.write(Path.of(tempDocBase.getAbsolutePath(), "delete_exist.txt"), "delete_exist_v0".getBytes(),
                    StandardOpenOption.CREATE);
            Path.of(tempDocBase.getAbsolutePath(), "delete_exist.txt").toFile().setLastModified(lastModified);
        }
    }

    @Test
    public void testPreconditions() throws Exception {
        Tomcat tomcat = getTomcatInstance();
        Context ctxt = tomcat.addContext("", tempDocBase.getAbsolutePath());

        Wrapper w = Tomcat.addServlet(ctxt, "default", DefaultServlet.class.getName());
        w.addInitParameter("readonly", "false");
        w.addInitParameter("allowPartialPut", "true");
        w.addInitParameter("useStrongETags", Boolean.toString(useStrongETags));
        ctxt.addServletMappingDecoded("/", "default");

        tomcat.start();

        Assert.assertNotNull(task);

        Map<String,List<String>> requestHeaders = new HashMap<>();
        Map<String,List<String>> responseHeaders = new HashMap<>();
        String etag = null;
        long lastModified = -1;
        String uri = "http://localhost:" + getPort() + task.uri;

        // Try head to receives etag and lastModified Date
        int sc = headUrl(uri, new ByteChunk(), responseHeaders);
        if (sc == 200) {
            etag = getSingleHeader("ETag", responseHeaders);
            String dt = getSingleHeader("Last-Modified", responseHeaders);
            if (dt != null && dt.length() > 0) {
                lastModified = FastHttpDateFormat.parseDate(dt);
            }
        }

        addPreconditionHeaders(requestHeaders, etag, lastModified);

        responseHeaders.clear();
        sc = 0;
        SimpleHttpClient client = null;
        client = new SimpleHttpClient() {

            @Override
            public boolean isResponseBodyOK() {
                return true;
            }
        };
        client.setPort(getPort());
        StringBuffer curl = new StringBuffer();
        curl.append(task.m.name() + " " + task.uri + " HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost" +
                SimpleHttpClient.CRLF + "Connection: Close" + SimpleHttpClient.CRLF);

        for (Entry<String,List<String>> e : requestHeaders.entrySet()) {
            for (String v : e.getValue()) {
                curl.append(e.getKey() + ": " + v + SimpleHttpClient.CRLF);
            }
        }
        if (addRangeHeader) {
            curl.append("Range: bytes=0-10" + SimpleHttpClient.CRLF);
        }
        curl.append("Content-Length: 6" + SimpleHttpClient.CRLF);
        curl.append(SimpleHttpClient.CRLF);

        curl.append("PUT_v2");
        client.setRequest(new String[] { curl.toString() });
        client.connect();
        client.processRequest();
        for (String e : client.getResponseHeaders()) {
            Assert.assertTrue("Separator ':' expected and not the last char of response header field `" + e + "`",
                    e.contains(":") && e.indexOf(':') < e.length() - 1);
            String name = e.substring(0, e.indexOf(':'));
            String value = e.substring(e.indexOf(':') + 1);
            responseHeaders.computeIfAbsent(name, k -> new ArrayList<String>()).add(value);
        }
        sc = client.getStatusCode();
        boolean test = scExpected.intValue() == sc;
        Assert.assertTrue("Failure - sc expected:" + String.valueOf(scExpected) + ", sc actual:" + String.valueOf(sc) + ", task:" +
                task + ", \ntarget resource:(" + etag + "," + FastHttpDateFormat.formatDate(lastModified) +
                "), \nreq headers: " + requestHeaders.toString() + ", \nresp headers: " + responseHeaders.toString(), test);
    }
}
