/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0 and the Server Side Public License, v 1; you may not use this file except
 * in compliance with, at your election, the Elastic License 2.0 or the Server
 * Side Public License, v 1.
 */
package org.elasticsearch.rest;

import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator;

import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.logging.DeprecationCategory;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.test.ESTestCase;

import org.junit.Before;
import org.mockito.InOrder;

import java.util.List;

import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Tests {@link DeprecationRestHandler}.
 */
public class DeprecationRestHandlerTests extends ESTestCase {

    private RestHandler handler;
    /**
     * Note: Headers should only use US ASCII (and this inevitably becomes one!).
     */
    private final String deprecationMessage = randomAlphaOfLengthBetween(1, 30);
    private DeprecationLogger deprecationLogger;
    private static final RestRequest.Method METHOD = RestRequest.Method.GET;
    private static final String PATH = "/some/path";

    @Before
    public void setup() {
        handler = mock(RestHandler.class);
        deprecationLogger = mock(DeprecationLogger.class);
    }

    public void testNullHandler() {
        expectThrows(NullPointerException.class, () -> new DeprecationRestHandler(null, METHOD, PATH, deprecationMessage,
            deprecationLogger, false));
    }

    public void testInvalidDeprecationMessageThrowsException() {
        String invalidDeprecationMessage = randomFrom("", null, "     ");

        expectThrows(IllegalArgumentException.class,
                     () -> new DeprecationRestHandler(handler, METHOD, PATH, invalidDeprecationMessage, deprecationLogger, false));
    }

    public void testNullDeprecationLogger() {
        expectThrows(NullPointerException.class, () -> new DeprecationRestHandler(handler, METHOD, PATH, deprecationMessage, null, false));
    }

    public void testHandleRequestLogsThenForwards() throws Exception {
        for (boolean compatibleVersionWarning : List.of(true, false)) {
            RestRequest request = mock(RestRequest.class);
            RestChannel channel = mock(RestChannel.class);
            NodeClient client = mock(NodeClient.class);

            DeprecationRestHandler deprecatedHandler = new DeprecationRestHandler(handler, METHOD, PATH, deprecationMessage,
                deprecationLogger, compatibleVersionWarning);

            // test it
            deprecatedHandler.handleRequest(request, channel, client);

            InOrder inOrder = inOrder(handler, request, channel, deprecationLogger);

            // log, then forward
            if (compatibleVersionWarning) {
                inOrder.verify(deprecationLogger)
                    .compatibleApiWarning("deprecated_route_GET_/some/path", deprecationMessage);
            } else {
                inOrder.verify(deprecationLogger)
                    .deprecate(DeprecationCategory.API, "deprecated_route_GET_/some/path", deprecationMessage);
            }

            inOrder.verify(handler).handleRequest(request, channel, client);
            inOrder.verifyNoMoreInteractions();
        }
    }

    public void testValidHeaderValue() {
        ASCIIHeaderGenerator generator = new ASCIIHeaderGenerator();
        String value = generator.ofCodeUnitsLength(random(), 1, 50);

        if (value.trim().length() == 0) {
            // empty text, not a valid header
            assertFalse(DeprecationRestHandler.validHeaderValue(value));
            Exception e = expectThrows(IllegalArgumentException.class, () -> DeprecationRestHandler.requireValidHeader(value));
            assertEquals("header value must contain only US ASCII text", e.getMessage());
        } else {
            assertTrue(DeprecationRestHandler.validHeaderValue(value));
            assertSame(value, DeprecationRestHandler.requireValidHeader(value));
        }
    }

    public void testInvalidHeaderValue() {
        ASCIIHeaderGenerator generator = new ASCIIHeaderGenerator();
        String value = generator.ofCodeUnitsLength(random(), 0, 25) +
                       randomFrom('\t', '\0', '\n', (char)27 /* ESC */, (char)31 /* unit separator*/, (char)127 /* DEL */) +
                       generator.ofCodeUnitsLength(random(), 0, 25);

        assertFalse(DeprecationRestHandler.validHeaderValue(value));

        expectThrows(IllegalArgumentException.class, () -> DeprecationRestHandler.requireValidHeader(value));
    }

    public void testInvalidHeaderValueNull() {
        assertFalse(DeprecationRestHandler.validHeaderValue(null));

        expectThrows(IllegalArgumentException.class, () -> DeprecationRestHandler.requireValidHeader(null));
    }

    public void testInvalidHeaderValueEmpty() {
        String blank = randomFrom("", "\t", "    ");

        assertFalse(DeprecationRestHandler.validHeaderValue(blank));

        expectThrows(IllegalArgumentException.class, () -> DeprecationRestHandler.requireValidHeader(blank));
    }

    public void testSupportsContentStreamTrue() {
        when(handler.supportsContentStream()).thenReturn(true);
        assertTrue(new DeprecationRestHandler(handler, METHOD, PATH, deprecationMessage, deprecationLogger,
            false).supportsContentStream());
    }

    public void testSupportsContentStreamFalse() {
        when(handler.supportsContentStream()).thenReturn(false);
        assertFalse(new DeprecationRestHandler(handler, METHOD, PATH, deprecationMessage, deprecationLogger,
            false).supportsContentStream());
    }

    /**
     * {@code ASCIIHeaderGenerator} only uses characters expected to be valid in headers (simplified US-ASCII).
     */
    private static class ASCIIHeaderGenerator extends CodepointSetGenerator {
        /**
         * Create a character array for characters [{@code from}, {@code to}].
         *
         * @param from Starting code point (inclusive).
         * @param to Ending code point (inclusive).
         * @return Never {@code null}.
         */
        static char[] asciiFromTo(int from, int to) {
            char[] chars = new char[to - from + 1];

            for (int i = from; i <= to; ++i) {
                chars[i - from] = (char)i;
            }

            return chars;
        }

        /**
         * Create a generator for characters [32, 126].
         */
        ASCIIHeaderGenerator() {
            super(asciiFromTo(32, 126));
        }
    }

}
