/*
 * 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.commons.validator.routines;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.validator.routines.IBANValidator.Validator;
import org.apache.commons.validator.routines.checkdigit.IBANCheckDigit;
import org.junit.Test;

/**
 * IBANValidator Test Case.
 * @since 1.5.0
 */
public class IBANValidatorTest {

    // It's not clear whether IBANs can contain lower case characters
    // so we test for both where possible
    // Note that the BIC near the start of the code is always upper case or digits
    private final String[] validIBANFormat = new String[] {
            "AD1200012030200359100100",
            "AE070331234567890123456",
            "AL47212110090000000235698741",
            "AT611904300234573201",
            "AZ21NABZ00000000137010001944",
            "BA391290079401028494",
            "BE68539007547034",
            "BG80BNBG96611020345678",
            "BH67BMAG00001299123456",
            "BR1800000000141455123924100C2",
            "BR1800360305000010009795493C1",
            "BR9700360305000010009795493P1",
            "BY13NBRB3600900000002Z00AB00",
            "CH9300762011623852957",
            "CR05015202001026284066",
            "CY17002001280000001200527600",
            "CZ6508000000192000145399",
            "CZ9455000000001011038930",
            "DE89370400440532013000",
            "DK5000400440116243",
            "DO28BAGR00000001212453611324",
            "EE382200221020145685",
            "EG380019000500000000263180002",
            "ES9121000418450200051332",
            "FI2112345600000785",
            "FI5542345670000081",
            "FO6264600001631634",
            "FR1420041010050500013M02606",
            "GB29NWBK60161331926819",
            "GE29NB0000000101904917",
            "GI75NWBK000000007099453",
            "GL8964710001000206",
            "GR1601101250000000012300695",
            "GT82TRAJ01020000001210029690",
            "HR1210010051863000160",
            "HU42117730161111101800000000",
            "IE29AIBK93115212345678",
            "IL620108000000099999999",
            "IQ98NBIQ850123456789012",
            "IS140159260076545510730339",
            "IT60X0542811101000000123456",
            "JO94CBJO0010000000000131000302",
            "KW81CBKU0000000000001234560101",
            "KZ86125KZT5004100100",
            "LB62099900000001001901229114",
            "LC55HEMM000100010012001200023015",
            "LI21088100002324013AA",
            "LT121000011101001000",
            "LU280019400644750000",
            "LV80BANK0000435195001",
            "MC5811222000010123456789030",
            "MD24AG000225100013104168",
            "ME25505000012345678951",
            "MK07250120000058984",
            "MR1300020001010000123456753",
            "MT84MALT011000012345MTLCAST001S",
            "MU17BOMM0101101030300200000MUR",
            "NL91ABNA0417164300",
            "NO9386011117947",
            "PK36SCBL0000001123456702",
            "PL61109010140000071219812874",
            "PS92PALS000000000400123456702",
            "PT50000201231234567890154",
            "QA58DOHB00001234567890ABCDEFG",
            "RO49AAAA1B31007593840000",
            "RS35260005601001611379",
            "SA0380000000608010167519",
            "SC18SSCB11010000000000001497USD",
            "SE4550000000058398257466",
            "SI56191000000123438",
            "SI56263300012039086",
            "SK3112000000198742637541",
            "SM86U0322509800000000270100",
            "ST68000100010051845310112",
            "SV62CENR00000000000000700025",
            "SV43ACAT00000000000000123123",
            "TL380080012345678910157",
            "TN5910006035183598478831",
            "TR330006100519786457841326",
            "UA213223130000026007233566001",
            "UA213996220000026007233566001",
            "VA59001123000012345678",
            "VG96VPVG0000012345678901",
            "XK051212012345678906",
    };

    private final String[] invalidIBANFormat = new String[] {
            "",                        // empty
            "   ",                     // empty
            "A",                       // too short
            "AB",                      // too short
            "FR1420041010050500013m02606", // lowercase version
            "MT84MALT011000012345mtlcast001s", // lowercase version
            "LI21088100002324013aa", // lowercase version
            "QA58DOHB00001234567890abcdefg", // lowercase version
            "RO49AAAA1b31007593840000", // lowercase version
            "LC62HEMM000100010012001200023015", // wrong in SWIFT
            "BY00NBRB3600000000000Z00AB00", // Wrong in SWIFT v73
            "ST68000200010192194210112", // ditto
            "SV62CENR0000000000000700025", // ditto
    };

    private static final IBANValidator VALIDATOR = IBANValidator.getInstance();

    @Test
    public void testValid() {
        for(String f : validIBANFormat) {
            assertTrue("Checksum fail: "+f, IBANCheckDigit.IBAN_CHECK_DIGIT.isValid(f));
            assertTrue("Missing validator: "+f, VALIDATOR.hasValidator(f));
            assertTrue(f, VALIDATOR.isValid(f));
        }
    }

    @Test
    public void testInValid() {
        for(String f : invalidIBANFormat) {
            assertFalse(f, VALIDATOR.isValid(f));
        }
    }

    @Test
    public void testNull() {
        assertFalse("isValid(null)",  VALIDATOR.isValid(null));
    }

    @Test
    public void testHasValidator() {
        assertTrue("GB", VALIDATOR.hasValidator("GB"));
        assertFalse("gb", VALIDATOR.hasValidator("gb"));
    }

    @Test
    public void testGetValidator() {
        assertNotNull("GB", VALIDATOR.getValidator("GB"));
        assertNull("gb", VALIDATOR.getValidator("gb"));        
    }

    @Test(expected=IllegalStateException.class)
    public void testSetDefaultValidator1() {
        assertNotNull(VALIDATOR.setValidator("GB", 15, "GB"));
    }

    @Test(expected=IllegalStateException.class)
    public void testSetDefaultValidator2() {
        assertNotNull(VALIDATOR.setValidator("GB", -1, "GB"));
    }

    @Test(expected=IllegalArgumentException.class)
    public void testSetValidatorLC() {
        IBANValidator validator = new IBANValidator();
        assertNotNull(validator.setValidator("gb", 15, "GB"));
    }

    @Test(expected=IllegalArgumentException.class)
    public void testSetValidatorLen7() {
        IBANValidator validator = new IBANValidator();
        assertNotNull(validator.setValidator("GB", 7, "GB"));
    }

    @Test(expected=IllegalArgumentException.class)
    public void testSetValidatorLen35() {
        IBANValidator validator = new IBANValidator();
        assertNotNull(validator.setValidator("GB", 35, "GB")); // valid params, but immutable validator
    }

    @Test
    public void testSetValidatorLen_1() {
        IBANValidator validator = new IBANValidator();
        assertNotNull("should be present",validator.setValidator("GB", -1, ""));
        assertNull("no longer present",validator.setValidator("GB", -1, ""));
    }

    @Test
    public void testSorted() {
        IBANValidator validator = new IBANValidator();
        Validator[] vals = validator.getDefaultValidators();
        assertNotNull(vals);
        for(int i=1; i < vals.length; i++) {
            if (vals[i].countryCode.compareTo(vals[i-1].countryCode) <= 0) {
                fail("Not sorted: "+vals[i].countryCode+ " <= " + vals[i-1].countryCode);
            }
        }
    }

    private static int checkIBAN(File file, IBANValidator val) throws Exception {
        // The IBAN Registry (TXT) file is a TAB-separated file
        // Rows are the entry types, columns are the countries
        CSVFormat format = CSVFormat.DEFAULT.withDelimiter('\t');
        Reader rdr = new InputStreamReader(new FileInputStream(file), "ISO_8859_1");
        CSVParser p = new CSVParser(rdr, format);
        CSVRecord country = null;
        CSVRecord cc = null;
        CSVRecord structure = null;
        CSVRecord length = null;
        for (CSVRecord o : p) {
            String item = o.get(0);
            if ("Name of country".equals(item)) {
                country = o;
            } else if ("IBAN prefix country code (ISO 3166)".equals(item)) {
                cc = o;
            } else if ("IBAN structure".equals(item)) {
                structure = o;
            } else if ("IBAN length".equals(item)) {
                length = o;
            }
        }
        for (int i=1; i < country.size(); i++) {
          try {

            String newLength = length.get(i).split("!")[0]; // El Salvador currently has "28!n"
            String newRE = fmtRE(structure.get(i), Integer.parseInt(newLength));
            final Validator valre = val.getValidator(cc.get(i));
            if (valre == null) {
                System.out.println("// Missing entry:");
                printEntry(
                        cc.get(i),
                        newLength,
                        newRE,
                        country.get(i));              
            } else {
                String currentLength = Integer.toString(valre.lengthOfIBAN);
                String currentRE = valre.validator.toString()
                        .replaceAll("^.+?\\{(.+)}","$1") // Extract RE from RegexValidator{re} string
                        .replaceAll("\\\\d","\\\\\\\\d"); // convert \d to \\d
                // The above assumes that the RegexValidator contains a single Regex
                if (currentRE.equals(newRE) && currentLength.equals(newLength)) {

                } else {
                    System.out.println("// Expected: " + newRE + ", " + newLength + " Actual: " + currentRE + ", " + currentLength);
                    printEntry(
                            cc.get(i),
                            newLength,
                            newRE,
                            country.get(i));              
                }

            }

          } catch (IllegalArgumentException e) {
            e.printStackTrace();
          }
        }
        p.close();
        return country.size();
    }

    private static void printEntry(String ccode, String length, String ib, String country) {
        String fmt = String.format("\"%s\"", ib);
        System.out.printf("            new Validator(\"%s\", %s, %-40s), // %s\n",
                ccode,
                length,
                fmt,
                country);        
    }

    // Unfortunately Java only returns the last match of repeated patterns
    // Use a manual repeat instead
    private static final String IBAN_PART = "(?:(\\d+)!([acn]))"; // Assume all parts are fixed length
    private static final Pattern IBAN_PAT = Pattern.compile(
            "([A-Z]{2})"+IBAN_PART+IBAN_PART+IBAN_PART+IBAN_PART+"?"+IBAN_PART+"?"+IBAN_PART+"?"+IBAN_PART+"?");

    // convert IBAN type string and length to regex
    private static String formatToRE(String type, int len) {
        char ctype = type.charAt(0); // assume type.length() == 1
        switch(ctype) {
        case 'n':
            return String.format("\\\\d{%d}",len);
        case 'a':
            return String.format("[A-Z]{%d}",len);
        case 'c':
            return String.format("[A-Z0-9]{%d}",len);
        default:
            throw new IllegalArgumentException("Unexpected type " + type);
        }
    }

    private static String fmtRE(String iban_pat, int iban_len) {
        Matcher m = IBAN_PAT.matcher(iban_pat);
        if (m.matches()) {
            StringBuilder sb = new StringBuilder();
            String cc = m.group(1); // country code
            int totalLen = cc.length();
            sb.append(cc);
            int len = Integer.parseInt(m.group(2)); // length of part
            String curType = m.group(3); // part type
            for (int i = 4; i <= m.groupCount(); i += 2) {
                if (m.group(i) == null) { // reached an optional group
                    break;
                }
                int count = Integer.parseInt(m.group(i));
                String type = m.group(i+1);
                if (type.equals(curType)) { // more of the same type
                    len += count;                    
                } else {
                    sb.append(formatToRE(curType,len));
                    totalLen += len;
                    curType = type;
                    len = count;
                }
            }
            sb.append(formatToRE(curType,len));
            totalLen += len;
            if (iban_len != totalLen) {
                throw new IllegalArgumentException("IBAN pattern " + iban_pat + " does not match length " + iban_len);                
            }
            return sb.toString();
        } else {
            throw new IllegalArgumentException("Unexpected IBAN pattern " + iban_pat);
        }
    }

    public static void main(String [] a) throws Exception {
        IBANValidator validator = new IBANValidator();
        File iban_tsv = new File("target","iban-registry.tsv");
        int countries = 0;
        if (iban_tsv.canRead()) {
            countries = checkIBAN(iban_tsv, validator);
        } else {
            System.out.println("Please load the file " + iban_tsv.getCanonicalPath() + " from https://www.swift.com/standards/data-standards/iban");
        }
        System.out.println("Processed " + countries + " countries.");
    }
}
