/*
 * 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
 *
 *      https://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.commons.codec.language;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Stream;

import org.apache.commons.codec.AbstractStringEncoderTest;
import org.apache.commons.codec.EncoderException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.opentest4j.AssertionFailedError;

/**
 * Tests the {@link ColognePhonetic} class.
 *
 * <p>
 * Keep this file in UTF-8 encoding for proper Javadoc processing.
 * </p>
 */
class ColognePhoneticTest extends AbstractStringEncoderTest<ColognePhonetic> {

    private static final Set<String> TESTSET = new HashSet<>();

    /** Character sequences to be tested by the code. */
    // @formatter:off
    private static final String[] MATCHES = {
            ".*[AEIOUJY].*",         // A, E, I, J, O, U, Y
            ".*H.*",                 // H
            ".*B.*",                 // B
            ".*P[^H].*",             // P not before H
            ".*[DT][^CSZ].*",        // D,T not before C,S,Z
            ".*[FVW].*",             // F,V,W
            ".*PH.*",                // P before H
            ".*[GKQ].*",             // G,K,Q
            "C[AHKLOQRUX].*",        // Initial C before A, H, K, L, O, Q, R, U, X
            ".*[^SZ]C[AHKLOQRUX].*", // C before A, H, K, L, O, Q, R, U, X but not after S, Z
            ".*[^CKQ]X.*",           // X not after C,K,Q
            ".*L.*",                 // L
            ".*[MN].*",              // M,N
            ".*R.*",                 // R
            ".*[SZ].*",              // S,Z
            ".*[SZ]C.*",             // C after S,Z
            "C[^AHKLOQRUX].*",       // Initial C except before A, H, K, L, O, Q, R, U, X
            ".+C[^AHKLOQRUX].*",     // C except before A, H, K, L, O, Q, R, U, X
            ".*[DT][CSZ].*",         // D,T before C,S,Z
            ".*[CKQ]X.*",            // X after C,K,Q
            // @formatter:on
    };

    @AfterAll
    // Check that all possible input sequence conditions are represented
    static void finishTests() {
        int errors = 0;
        for (final String m : MATCHES) {
            if (!hasTestCase(m)) {
                System.out.println(m + " has no test case");
                errors++;
            }
        }
        assertEquals(0, errors, "Not expecting any missing test cases");
    }

    private static boolean hasTestCase(final String re) {
        for (final String s : TESTSET) {
            if (s.matches(re)) {
                return true;
            }
        }
        return false;
    }

    // Allow command-line testing
    public static void main(final String[] args) {
        final ColognePhonetic coder = new ColognePhonetic();
        for (final String arg : args) {
            final String code = coder.encode(arg);
            System.out.println("'" + arg + "' = '" + code + "'");
        }
    }

    static Stream<Arguments> testBasicEncoding() {
        // @formatter:off
        return Stream.of(
            Arguments.arguments("01", "Aabjoe"),
            Arguments.arguments("0856", "Aaclan"),
            Arguments.arguments("04567", "Aychlmajr") // CODEC-122
        );
        // @formatter:on
    }

    static Stream<Arguments> testEdgeCases() {
        // @formatter:off
        return Stream.of(
            Arguments.arguments("a", "0"),
            Arguments.arguments("e", "0"),
            Arguments.arguments("i", "0"),
            Arguments.arguments("o", "0"),
            Arguments.arguments("u", "0"),
            Arguments.arguments("\u00E4", "0"), // a-umlaut
            Arguments.arguments("\u00F6", "0"), // o-umlaut
            Arguments.arguments("\u00FC", "0"), // u-umlaut
            Arguments.arguments("\u00DF", "8"), // small sharp s
            Arguments.arguments("aa", "0"),
            Arguments.arguments("ha", "0"),
            Arguments.arguments("h", ""),
            Arguments.arguments("aha", "0"),
            Arguments.arguments("b", "1"),
            Arguments.arguments("p", "1"),
            Arguments.arguments("ph", "3"),
            Arguments.arguments("f", "3"),
            Arguments.arguments("v", "3"),
            Arguments.arguments("w", "3"),
            Arguments.arguments("g", "4"),
            Arguments.arguments("k", "4"),
            Arguments.arguments("q", "4"),
            Arguments.arguments("x", "48"),
            Arguments.arguments("ax", "048"),
            Arguments.arguments("cx", "48"),
            Arguments.arguments("l", "5"),
            Arguments.arguments("cl", "45"),
            Arguments.arguments("acl", "085"),
            Arguments.arguments("mn", "6"),
            Arguments.arguments("{mn}", "6"), // test chars above Z
            Arguments.arguments("r", "7")
        );
        // @formatter:on
    }

    static Stream<Arguments> testExamples() {
        // @formatter:off
        return Stream.of(
            Arguments.arguments("m\u00DCller", "657"), // mÜller - why upper case U-umlaut?
            Arguments.arguments("m\u00FCller", "657"), // müller - add equivalent lower-case
            Arguments.arguments("schmidt", "862"),
            Arguments.arguments("schneider", "8627"),
            Arguments.arguments("fischer", "387"),
            Arguments.arguments("weber", "317"),
            Arguments.arguments("wagner", "3467"),
            Arguments.arguments("becker", "147"),
            Arguments.arguments("hoffmann", "036"),
            Arguments.arguments("sch\u00C4fer", "837"), // schÄfer - why upper case A-umlaut ?
            Arguments.arguments("sch\u00e4fer", "837"), // schäfer - add equivalent lower-case
            Arguments.arguments("Breschnew", "17863"),
            Arguments.arguments("Wikipedia", "3412"),
            Arguments.arguments("peter", "127"),
            Arguments.arguments("pharma", "376"),
            Arguments.arguments("m\u00f6nchengladbach", "64645214"), // mönchengladbach
            Arguments.arguments("deutsch", "28"),
            Arguments.arguments("deutz", "28"),
            Arguments.arguments("hamburg", "06174"),
            Arguments.arguments("hannover", "0637"),
            Arguments.arguments("christstollen", "478256"),
            Arguments.arguments("Xanthippe", "48621"),
            Arguments.arguments("Zacharias", "8478"),
            Arguments.arguments("Holzbau", "0581"),
            Arguments.arguments("matsch", "68"),
            Arguments.arguments("matz", "68"),
            Arguments.arguments("Arbeitsamt", "071862"),
            Arguments.arguments("Eberhard", "0172"),
            Arguments.arguments("Eberhardt", "0172"),
            Arguments.arguments("Celsius", "858"),
            Arguments.arguments("Ace", "08"),
            Arguments.arguments("shch", "84"), // CODEC-254
            Arguments.arguments("xch", "484"), // CODEC-255
            Arguments.arguments("heithabu", "021")
        );
        // @formatter:on
    }

    static Stream<Arguments> testIsEncodeEquals() {
        // @formatter:off
        return Stream.of(
            Arguments.arguments("Muller", "M\u00fcller"), // Müller
            Arguments.arguments("Meyer", "Mayr"),
            Arguments.arguments("house", "house"),
            Arguments.arguments("House", "house"),
            Arguments.arguments("Haus", "house"),
            Arguments.arguments("ganz", "Gans"),
            Arguments.arguments("ganz", "G\u00e4nse"), // Gänse
            Arguments.arguments("Miyagi", "Miyako")
        );
        // @formatter:on
    }

    @Override
    // Capture test strings for later checking
    public void checkEncoding(final String expected, final String source) throws EncoderException {
        // Note that the German letter Eszett is converted to SS by toUpperCase, so we don't need to replace it
        TESTSET.add(source.toUpperCase(Locale.GERMAN).replace('Ä', 'A').replace('Ö', 'O').replace('Ü', 'U'));
        super.checkEncoding(expected, source);
    }

    @Override
    protected ColognePhonetic createStringEncoder() {
        return new ColognePhonetic();
    }

    @ParameterizedTest
    @MethodSource
    void testBasicEncoding(final String expected, final String source) throws EncoderException {
        checkEncoding(expected, source);
    }

    @Test
    // Ensure that override still allows tests to work
    void testCanFail() {
        assertThrows(AssertionFailedError.class, () -> checkEncoding("/", "Fehler"));
    }

    @ParameterizedTest
    @MethodSource
    void testEdgeCases(final String source, final String expected) throws EncoderException {
        checkEncoding(expected, source);
    }

    @ParameterizedTest
    @MethodSource
    void testExamples(final String source, final String expected) throws EncoderException {
        checkEncoding(expected, source);
    }

    @Test
    void testHyphen() throws EncoderException {
        // Müller-Lüdenscheidt
        checkEncodings(new String[][] { { "bergisch-gladbach", "174845214" }, { "M\u00fcller-L\u00fcdenscheidt", "65752682" } });
    }

    @ParameterizedTest
    @MethodSource
    void testIsEncodeEquals(final String source, final String expected) {
        final boolean encodeEqual = getStringEncoder().isEncodeEqual(expected, source);
        assertTrue(encodeEqual, () -> expected + " != " + source);
    }

    @Test
    void testSpecialCharsBetweenSameLetters() throws EncoderException {
        checkEncodingVariations("28282", "Test test", "Testtest", "Test-test", "TesT#Test", "TesT?test");
    }

    @Test
    void testVariationsMella() throws EncoderException {
        checkEncodingVariations("65", "mella", "milah", "moulla", "mellah", "muehle", "mule");
    }

    @Test
    void testVariationsMeyer() throws EncoderException {
        checkEncodingVariations("67", "Meier", "Maier", "Mair", "Meyer", "Meyr", "Mejer", "Major");
    }
}
