package org.spongycastle.asn1.test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Vector;

import org.spongycastle.asn1.ASN1Encodable;
import org.spongycastle.asn1.ASN1EncodableVector;
import org.spongycastle.asn1.ASN1InputStream;
import org.spongycastle.asn1.ASN1ObjectIdentifier;
import org.spongycastle.asn1.ASN1OutputStream;
import org.spongycastle.asn1.ASN1Primitive;
import org.spongycastle.asn1.ASN1Sequence;
import org.spongycastle.asn1.ASN1Set;
import org.spongycastle.asn1.DERGeneralizedTime;
import org.spongycastle.asn1.DERIA5String;
import org.spongycastle.asn1.DERObjectIdentifier;
import org.spongycastle.asn1.DERPrintableString;
import org.spongycastle.asn1.DERSequence;
import org.spongycastle.asn1.DERSet;
import org.spongycastle.asn1.DERUTF8String;
import org.spongycastle.asn1.x500.X500Name;
import org.spongycastle.asn1.x500.X500NameBuilder;
import org.spongycastle.asn1.x500.style.BCStyle;
import org.spongycastle.asn1.x509.X509DefaultEntryConverter;
import org.spongycastle.asn1.x509.X509Name;
import org.spongycastle.util.Arrays;
import org.spongycastle.util.encoders.Hex;
import org.spongycastle.util.test.SimpleTest;

public class X509NameTest
    extends SimpleTest
{
   String[] subjects =
   {
       "C=AU,ST=Victoria,L=South Melbourne,O=Connect 4 Pty Ltd,OU=Webserver Team,CN=www2.connect4.com.au,E=webmaster@connect4.com.au",
       "C=AU,ST=Victoria,L=South Melbourne,O=Connect 4 Pty Ltd,OU=Certificate Authority,CN=Connect 4 CA,E=webmaster@connect4.com.au",
       "C=AU,ST=QLD,CN=SSLeay/rsa test cert",
       "C=US,O=National Aeronautics and Space Administration,SERIALNUMBER=16+CN=Steve Schoch",
       "E=cooke@issl.atl.hp.com,C=US,OU=Hewlett Packard Company (ISSL),CN=Paul A. Cooke",
       "O=Sun Microsystems Inc,CN=store.sun.com",
       "unstructuredAddress=192.168.1.33,unstructuredName=pixfirewall.ciscopix.com,CN=pixfirewall.ciscopix.com",
       "CN=*.canal-plus.com,OU=Provided by TBS INTERNET http://www.tbs-certificats.com/,OU=\\ CANAL \\+,O=CANAL\\+DISTRIBUTION,L=issy les moulineaux,ST=Hauts de Seine,C=FR",
       "O=Bouncy Castle,CN=www.bouncycastle.org\\ ",
       "O=Bouncy Castle,CN=c:\\\\fred\\\\bob"
    };

    public String getName()
    {
        return "X509Name";
    }
    
    private static X509Name fromBytes(
        byte[]  bytes) 
        throws IOException
    {
        return X509Name.getInstance(new ASN1InputStream(new ByteArrayInputStream(bytes)).readObject());
    }

    private ASN1Encodable createEntryValue(DERObjectIdentifier oid, String value)
    {
        Hashtable attrs = new Hashtable();

        attrs.put(oid, value);

        Vector order = new Vector();

        order.addElement(oid);
        
        X509Name name = new X509Name(order, attrs);

        ASN1Sequence seq = (ASN1Sequence)name.toASN1Primitive();
        ASN1Set set = (ASN1Set)seq.getObjectAt(0);
        seq = (ASN1Sequence)set.getObjectAt(0);

        return seq.getObjectAt(1);
    }

    private ASN1Encodable createEntryValueFromString(DERObjectIdentifier oid, String value)
    {
        Hashtable attrs = new Hashtable();

        attrs.put(oid, value);

        Vector order = new Vector();

        order.addElement(oid);
        
        X509Name name = new X509Name(new X509Name(order, attrs).toString());

        ASN1Sequence seq = (ASN1Sequence)name.toASN1Primitive();
        ASN1Set set = (ASN1Set)seq.getObjectAt(0);
        seq = (ASN1Sequence)set.getObjectAt(0);

        return seq.getObjectAt(1);
    }

    private void testEncodingPrintableString(DERObjectIdentifier oid, String value)
    {
        ASN1Encodable converted = createEntryValue(oid, value);
        if (!(converted instanceof DERPrintableString))
        {
            fail("encoding for " + oid + " not printable string");
        }
    }

    private void testEncodingIA5String(DERObjectIdentifier oid, String value)
    {
        ASN1Encodable converted = createEntryValue(oid, value);
        if (!(converted instanceof DERIA5String))
        {
            fail("encoding for " + oid + " not IA5String");
        }
    }


    private void testEncodingUTF8String(DERObjectIdentifier oid, String value)
        throws IOException
    {
        ASN1Encodable converted = createEntryValue(oid, value);
        if (!(converted instanceof DERUTF8String))
        {
            fail("encoding for " + oid + " not IA5String");
        }
        if (!value.equals((DERUTF8String.getInstance(converted.toASN1Primitive().getEncoded()).getString())))
        {
            fail("decoding not correct");
        }
    }

    private void testEncodingGeneralizedTime(DERObjectIdentifier oid, String value)
    {
        ASN1Encodable converted = createEntryValue(oid, value);
        if (!(converted instanceof DERGeneralizedTime))
        {
            fail("encoding for " + oid + " not GeneralizedTime");
        }
        converted = createEntryValueFromString(oid, value);
        if (!(converted instanceof DERGeneralizedTime))
        {
            fail("encoding for " + oid + " not GeneralizedTime");
        }
    }

    public void performTest()
        throws Exception
    {
        testEncodingPrintableString(X509Name.C, "AU");
        testEncodingPrintableString(X509Name.SERIALNUMBER, "123456");
        testEncodingPrintableString(X509Name.DN_QUALIFIER, "123456");
        testEncodingIA5String(X509Name.EmailAddress, "test@test.com");
        testEncodingIA5String(X509Name.DC, "test");
        // correct encoding
        testEncodingGeneralizedTime(X509Name.DATE_OF_BIRTH, "#180F32303032303132323132323232305A");
        // compatibility encoding
        testEncodingGeneralizedTime(X509Name.DATE_OF_BIRTH, "20020122122220Z");
        testEncodingUTF8String(X509Name.CN, "Mörsky");
        //
        // composite
        //
        Hashtable                   attrs = new Hashtable();

        attrs.put(X509Name.C, "AU");
        attrs.put(X509Name.O, "The Legion of the Bouncy Castle");
        attrs.put(X509Name.L, "Melbourne");
        attrs.put(X509Name.ST, "Victoria");
        attrs.put(X509Name.E, "feedback-crypto@bouncycastle.org");

        Vector                     order = new Vector();

        order.addElement(X509Name.C);
        order.addElement(X509Name.O);
        order.addElement(X509Name.L);
        order.addElement(X509Name.ST);
        order.addElement(X509Name.E);

        X509Name    name1 = new X509Name(order, attrs);

        if (!name1.equals(name1))
        {
            fail("Failed same object test");
        }

        if (!name1.equals(name1, true))
        {
            fail("Failed same object test - in Order");
        }

        X509Name    name2 = new X509Name(order, attrs);

        if (!name1.equals(name2))
        {
            fail("Failed same name test");
        }

        if (!name1.equals(name2, true))
        {
            fail("Failed same name test - in Order");
        }

        if (name1.hashCode() != name2.hashCode())
        {
            fail("Failed same name test - in Order");
        }

        Vector  ord1 = new Vector();

        ord1.addElement(X509Name.C);
        ord1.addElement(X509Name.O);
        ord1.addElement(X509Name.L);
        ord1.addElement(X509Name.ST);
        ord1.addElement(X509Name.E);

        Vector  ord2 = new Vector();

        ord2.addElement(X509Name.E);
        ord2.addElement(X509Name.ST);
        ord2.addElement(X509Name.L);
        ord2.addElement(X509Name.O);
        ord2.addElement(X509Name.C);

        name1 = new X509Name(ord1, attrs);
        name2 = new X509Name(ord2, attrs);

        if (!name1.equals(name2))
        {
            fail("Failed reverse name test");
        }

        if (name1.hashCode() != name2.hashCode())
        {
            fail("Failed reverse name test hashCode");
        }

        if (name1.equals(name2, true))
        {
            fail("Failed reverse name test - in Order");
        }

        if (!name1.equals(name2, false))
        {
            fail("Failed reverse name test - in Order false");
        }

        Vector oids = name1.getOIDs();
        if (!compareVectors(oids, ord1))
        {
            fail("oid comparison test");
        }

        Vector val1 = new Vector();

        val1.addElement("AU");
        val1.addElement("The Legion of the Bouncy Castle");
        val1.addElement("Melbourne");
        val1.addElement("Victoria");
        val1.addElement("feedback-crypto@bouncycastle.org");

        name1 = new X509Name(ord1, val1);
        
        Vector values = name1.getValues();
        if (!compareVectors(values, val1))
        {
            fail("value comparison test");
        }

        ord2 = new Vector();

        ord2.addElement(X509Name.ST);
        ord2.addElement(X509Name.ST);
        ord2.addElement(X509Name.L);
        ord2.addElement(X509Name.O);
        ord2.addElement(X509Name.C);

        name1 = new X509Name(ord1, attrs);
        name2 = new X509Name(ord2, attrs);

        if (name1.equals(name2))
        {
            fail("Failed different name test");
        }

        ord2 = new Vector();

        ord2.addElement(X509Name.ST);
        ord2.addElement(X509Name.L);
        ord2.addElement(X509Name.O);
        ord2.addElement(X509Name.C);

        name1 = new X509Name(ord1, attrs);
        name2 = new X509Name(ord2, attrs);

        if (name1.equals(name2))
        {
            fail("Failed subset name test");
        }

        compositeTest();

        ByteArrayOutputStream bOut;
        ASN1OutputStream aOut;
        ASN1InputStream aIn;

        //
        // getValues test
        //
        Vector v1 = name1.getValues(X509Name.O);

        if (v1.size() != 1 || !v1.elementAt(0).equals("The Legion of the Bouncy Castle"))
        {
            fail("O test failed");
        }

        Vector v2 = name1.getValues(X509Name.L);

        if (v2.size() != 1 || !v2.elementAt(0).equals("Melbourne"))
        {
            fail("L test failed");
        }

        //
        // general subjects test
        //
        for (int i = 0; i != subjects.length; i++)
        {
            X509Name    name = new X509Name(subjects[i]);

            bOut = new ByteArrayOutputStream();
            aOut = new ASN1OutputStream(bOut);

            aOut.writeObject(name);

            aIn = new ASN1InputStream(new ByteArrayInputStream(bOut.toByteArray()));

            name = X509Name.getInstance(aIn.readObject());

            if (!name.toString().equals(subjects[i]))
            {
                fail("failed regeneration test " + i + " got " + name.toString());
            }
        }

        //
        // sort test
        //
        X509Name unsorted = new X509Name("SERIALNUMBER=BBB + CN=AA");

        if (!fromBytes(unsorted.getEncoded()).toString().equals("CN=AA+SERIALNUMBER=BBB"))
        {
            fail("failed sort test 1");
        }

        unsorted = new X509Name("CN=AA + SERIALNUMBER=BBB");

        if (!fromBytes(unsorted.getEncoded()).toString().equals("CN=AA+SERIALNUMBER=BBB"))
        {
            fail("failed sort test 2");
        }

        unsorted = new X509Name("SERIALNUMBER=B + CN=AA");

        if (!fromBytes(unsorted.getEncoded()).toString().equals("SERIALNUMBER=B+CN=AA"))
        {
            fail("failed sort test 3");
        }

        unsorted = new X509Name("CN=AA + SERIALNUMBER=B");

        if (!fromBytes(unsorted.getEncoded()).toString().equals("SERIALNUMBER=B+CN=AA"))
        {
            fail("failed sort test 4");
        }

        //
        // equality tests
        //
        equalityTest(new X509Name("CN=The     Legion"), new X509Name("CN=The Legion"));
        equalityTest(new X509Name("CN=   The Legion"), new X509Name("CN=The Legion"));
        equalityTest(new X509Name("CN=The Legion   "), new X509Name("CN=The Legion"));
        equalityTest(new X509Name("CN=  The     Legion "), new X509Name("CN=The Legion"));
        equalityTest(new X509Name("CN=  the     legion "), new X509Name("CN=The Legion"));

        // # test

        X509Name n1 = new X509Name("SERIALNUMBER=8,O=ABC,CN=ABC Class 3 CA,C=LT");
        X509Name n2 = new X509Name("2.5.4.5=8,O=ABC,CN=ABC Class 3 CA,C=LT");
        X509Name n3 = new X509Name("2.5.4.5=#130138,O=ABC,CN=ABC Class 3 CA,C=LT");

        equalityTest(n1, n2);
        equalityTest(n2, n3);
        equalityTest(n3, n1);

        n1 = new X509Name(true, "2.5.4.5=#130138,CN=SSC Class 3 CA,O=UAB Skaitmeninio sertifikavimo centras,C=LT");
        n2 = new X509Name(true, "SERIALNUMBER=#130138,CN=SSC Class 3 CA,O=UAB Skaitmeninio sertifikavimo centras,C=LT");
        n3 = X509Name.getInstance(ASN1Primitive.fromByteArray(Hex.decode("3063310b3009060355040613024c54312f302d060355040a1326"
            + "55414220536b6169746d656e696e696f20736572746966696b6176696d6f2063656e74726173311730150603550403130e53534320436c6173732033204341310a30080603550405130138")));

        equalityTest(n1, n2);
        equalityTest(n2, n3);
        equalityTest(n3, n1);

        n1 = new X509Name("SERIALNUMBER=8,O=XX,CN=ABC Class 3 CA,C=LT");
        n2 = new X509Name("2.5.4.5=8,O=,CN=ABC Class 3 CA,C=LT");

        if (n1.equals(n2))
        {
            fail("empty inequality check failed");
        }

        n1 = new X509Name("SERIALNUMBER=8,O=,CN=ABC Class 3 CA,C=LT");
        n2 = new X509Name("2.5.4.5=8,O=,CN=ABC Class 3 CA,C=LT");

        equalityTest(n1, n2);
        
        //
        // inequality to sequences
        //
        name1 = new X509Name("CN=The Legion");

        if (name1.equals(new DERSequence()))
        {
            fail("inequality test with sequence");
        }

        if (name1.equals(new DERSequence(new DERSet())))
        {
            fail("inequality test with sequence and set");
        }

        ASN1EncodableVector v = new ASN1EncodableVector();

        v.add(new DERObjectIdentifier("1.1"));
        v.add(new DERObjectIdentifier("1.1"));
        if (name1.equals(new DERSequence(new DERSet(new DERSet(v)))))
        {
            fail("inequality test with sequence and bad set");
        }

        if (name1.equals(new DERSequence(new DERSet(new DERSet(v))), true))
        {
            fail("inequality test with sequence and bad set");
        }

        if (name1.equals(new DERSequence(new DERSet(new DERSequence()))))
        {
            fail("inequality test with sequence and short sequence");
        }

        if (name1.equals(new DERSequence(new DERSet(new DERSequence())), true))
        {
            fail("inequality test with sequence and short sequence");
        }

        v = new ASN1EncodableVector();

        v.add(new DERObjectIdentifier("1.1"));
        v.add(new DERSequence());

        if (name1.equals(new DERSequence(new DERSet(new DERSequence(v)))))
        {
            fail("inequality test with sequence and bad sequence");
        }

        if (name1.equals(null))
        {
            fail("inequality test with null");
        }

        if (name1.equals(null, true))
        {
            fail("inequality test with null");
        }

        //
        // this is contrived but it checks sorting of sets with equal elements
        //
        unsorted = new X509Name("CN=AA + CN=AA + CN=AA");

        //
        // tagging test - only works if CHOICE implemented
        //
        /*
        ASN1TaggedObject tag = new DERTaggedObject(false, 1, new X509Name("CN=AA"));

        if (!tag.isExplicit())
        {
            fail("failed to explicitly tag CHOICE object");
        }

        X509Name name = X509Name.getInstance(tag, false);

        if (!name.equals(new X509Name("CN=AA")))
        {
            fail("failed to recover tagged name");
        }
        */

        DERUTF8String testString = new DERUTF8String("The Legion of the Bouncy Castle");
        byte[] encodedBytes = testString.getEncoded();
        byte[] hexEncodedBytes = Hex.encode(encodedBytes);
        String hexEncodedString = "#" + new String(hexEncodedBytes);

        DERUTF8String converted = (DERUTF8String)
            new X509DefaultEntryConverter().getConvertedValue(
                X509Name.L , hexEncodedString);

        if (!converted.equals(testString))
        {
            fail("failed X509DefaultEntryConverter test");
        }

        //
        // try escaped.
        //
        converted = (DERUTF8String)
            new X509DefaultEntryConverter().getConvertedValue(
                X509Name.L , "\\" + hexEncodedString);

        if (!converted.equals(new DERUTF8String(hexEncodedString)))
        {
            fail("failed X509DefaultEntryConverter test got " + converted + " expected: " + hexEncodedString);
        }
        
        //
        // try a weird value
        //
        X509Name n = new X509Name("CN=\\#nothex#string");

        if (!n.toString().equals("CN=\\#nothex#string"))
        {
            fail("# string not properly escaped.");
        }

        Vector vls = n.getValues(X509Name.CN);
        if (vls.size() != 1 || !vls.elementAt(0).equals("#nothex#string"))
        {
            fail("escaped # not reduced properly");
        }

        n = new X509Name("CN=\"a+b\"");

        vls = n.getValues(X509Name.CN);
        if (vls.size() != 1 || !vls.elementAt(0).equals("a+b"))
        {
            fail("escaped + not reduced properly");
        }

        n = new X509Name("CN=a\\+b");

        vls = n.getValues(X509Name.CN);
        if (vls.size() != 1 || !vls.elementAt(0).equals("a+b"))
        {
            fail("escaped + not reduced properly");
        }

        if (!n.toString().equals("CN=a\\+b"))
        {
            fail("+ in string not properly escaped.");
        }

        n = new X509Name("CN=a\\=b");

        vls = n.getValues(X509Name.CN);
        if (vls.size() != 1 || !vls.elementAt(0).equals("a=b"))
        {
            fail("escaped = not reduced properly");
        }

        if (!n.toString().equals("CN=a\\=b"))
        {
            fail("= in string not properly escaped.");
        }

        n = new X509Name("TELEPHONENUMBER=\"+61999999999\"");

        vls = n.getValues(X509Name.TELEPHONE_NUMBER);
        if (vls.size() != 1 || !vls.elementAt(0).equals("+61999999999"))
        {
            fail("telephonenumber escaped + not reduced properly");
        }

        n = new X509Name("TELEPHONENUMBER=\\+61999999999");

        vls = n.getValues(X509Name.TELEPHONE_NUMBER);
        if (vls.size() != 1 || !vls.elementAt(0).equals("+61999999999"))
        {
            fail("telephonenumber escaped + not reduced properly");
        }

        // migration
        X500NameBuilder builder = new X500NameBuilder(BCStyle.INSTANCE);
        builder.addMultiValuedRDN(new ASN1ObjectIdentifier[] { BCStyle.CN, BCStyle.SN }, new String[] { "Thomas", "CVR:12341233-UID:1111" });
        builder.addRDN(BCStyle.O, "Test");
        builder.addRDN(BCStyle.C, "DK");

        X500Name subject = builder.build();
        ASN1Primitive derObject = subject.toASN1Primitive();
        X509Name instance = X509Name.getInstance(derObject);
    }

    private boolean compareVectors(Vector a, Vector b)    // for compatibility with early JDKs
    {
        if (a.size() != b.size())
        {
            return false;
        }

        for (int i = 0; i != a.size(); i++)
        {
            if (!a.elementAt(i).equals(b.elementAt(i)))
            {
                return false;
            }
        }

        return true;
    }

    private void compositeTest()
        throws IOException
    {
        //
        // composite test
        //
        byte[]  enc = Hex.decode("305e310b300906035504061302415531283026060355040a0c1f546865204c6567696f6e206f662074686520426f756e637920436173746c653125301006035504070c094d656c626f75726e653011060355040b0c0a4173636f742056616c65");
        ASN1InputStream aIn = new ASN1InputStream(new ByteArrayInputStream(enc));

        X509Name    n = X509Name.getInstance(aIn.readObject());

        if (!n.toString().equals("C=AU,O=The Legion of the Bouncy Castle,L=Melbourne+OU=Ascot Vale"))
        {
            fail("Failed composite to string test got: " + n.toString());
        }

        if (!n.toString(true, X509Name.DefaultSymbols).equals("L=Melbourne+OU=Ascot Vale,O=The Legion of the Bouncy Castle,C=AU"))
        {
            fail("Failed composite to string test got: " + n.toString(true, X509Name.DefaultSymbols));
        }

        n = new X509Name(true, "L=Melbourne+OU=Ascot Vale,O=The Legion of the Bouncy Castle,C=AU");
        if (!n.toString().equals("C=AU,O=The Legion of the Bouncy Castle,L=Melbourne+OU=Ascot Vale"))
        {
            fail("Failed composite to string reversal test got: " + n.toString());
        }

        n = new X509Name("C=AU, O=The Legion of the Bouncy Castle, L=Melbourne + OU=Ascot Vale");

        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        ASN1OutputStream aOut = new ASN1OutputStream(bOut);

        aOut.writeObject(n);

        byte[]  enc2 = bOut.toByteArray();

        if (!Arrays.areEqual(enc, enc2))
        {
            //fail("Failed composite string to encoding test");
        }
        
        //
        // dud name test - handle empty DN without barfing.
        //
        n = new X509Name("C=CH,O=,OU=dummy,CN=mail@dummy.com");

        n = X509Name.getInstance(ASN1Primitive.fromByteArray(n.getEncoded()));
    }

    private void equalityTest(X509Name x509Name, X509Name x509Name1)
    {
        if (!x509Name.equals(x509Name1))
        {
            fail("equality test failed for " + x509Name + " : " + x509Name1);
        }

        if (x509Name.hashCode() != x509Name1.hashCode())
        {
            fail("hashCodeTest test failed for " + x509Name + " : " + x509Name1);
        }

        if (!x509Name.equals(x509Name1, true))
        {
            fail("equality test failed for " + x509Name + " : " + x509Name1);
        }
    }


    public static void main(
        String[]    args)
    {
        runTest(new X509NameTest());
    }
}
