/*
Copyright 2023 The cert-manager Authors.

Licensed 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 pki

import (
	"encoding/asn1"
	"errors"
	"reflect"
	"testing"
)

func TestParseObjectIdentifier(t *testing.T) {
	testCases := []struct {
		oidString   string
		expectedOid asn1.ObjectIdentifier
		expectedErr error
	}{
		{
			oidString:   "1.2.3.4.5",
			expectedOid: asn1.ObjectIdentifier{1, 2, 3, 4, 5},
			expectedErr: nil,
		},
		{
			oidString:   "1.2.840.113549.1.1.1",
			expectedOid: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1},
			expectedErr: nil,
		},
		{
			oidString:   "1.3.6.1.4.1.311.60.2.1.3",
			expectedOid: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 60, 2, 1, 3},
			expectedErr: nil,
		},
		{
			oidString:   ".",
			expectedOid: nil,
			expectedErr: errors.New("strconv.Atoi: parsing \"\": invalid syntax"),
		},
		{
			oidString:   ".555",
			expectedOid: nil,
			expectedErr: errors.New("strconv.Atoi: parsing \"\": invalid syntax"),
		},
		{
			oidString:   "555.",
			expectedOid: nil,
			expectedErr: errors.New("strconv.Atoi: parsing \"\": invalid syntax"),
		},
		{
			oidString:   "test.5",
			expectedOid: nil,
			expectedErr: errors.New("strconv.Atoi: parsing \"test\": invalid syntax"),
		},
	}

	for _, tc := range testCases {
		oid, err := ParseObjectIdentifier(tc.oidString)
		if err != nil {
			if tc.expectedErr == nil {
				t.Errorf("Unexpected error: %v", err)
			} else if err.Error() != tc.expectedErr.Error() {
				t.Errorf("Expected error: %v, got: %v", tc.expectedErr, err)
			}
		} else if !oid.Equal(tc.expectedOid) {
			t.Errorf("Expected OID: %v, got: %v", tc.expectedOid, oid)
		}
	}
}

func TestMarshalAndUnmarshalUniversalValue(t *testing.T) {
	testCases := []struct {
		name                string
		uv                  UniversalValue
		raw                 asn1.RawValue
		overrideRoundtripUv *UniversalValue
	}{
		{
			name: "Test with IA5String",
			uv: UniversalValue{
				IA5String: "test",
			},
			raw: asn1.RawValue{
				Bytes: []byte("test"),
				Class: asn1.ClassUniversal,
				Tag:   asn1.TagIA5String,
			},
		},
		{
			name: "Test with Utf8String",
			uv: UniversalValue{
				UTF8String: "test",
			},
			raw: asn1.RawValue{
				Bytes: []byte("test"),
				Class: asn1.ClassUniversal,
				Tag:   asn1.TagUTF8String,
			},
		},
		{
			name: "Test with PrintableString",
			uv: UniversalValue{
				PrintableString: "test",
			},
			raw: asn1.RawValue{
				Bytes: []byte("test"),
				Class: asn1.ClassUniversal,
				Tag:   asn1.TagPrintableString,
			},
		},
		{
			name: "Test with Bytes",
			uv: UniversalValue{
				// Ia5String byte array with value "test"
				// https://lapo.it/asn1js/#FgR0ZXN0
				Bytes: []byte{0x16, 0x04, 0x74, 0x65, 0x73, 0x74},
			},
			overrideRoundtripUv: &UniversalValue{
				IA5String: "test",
			},
			raw: asn1.RawValue{
				Bytes: []byte("test"),
				Class: asn1.ClassUniversal,
				Tag:   asn1.TagIA5String,
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			{
				rawValue, err := MarshalUniversalValue(tc.uv)
				if err != nil {
					t.Errorf("Unexpected error: %v", err)
				}

				// Calculate fullBytes
				fullBytes, err := asn1.Marshal(tc.raw)
				if err != nil {
					t.Errorf("Unexpected error: %v", err)
				}

				if !reflect.DeepEqual(rawValue, fullBytes) {
					t.Errorf("Expected rawValue: %v, got: %v", fullBytes, rawValue)
				}
			}

			{
				uv, err := UnmarshalUniversalValue(tc.raw)
				if err != nil {
					t.Errorf("Unexpected error: %v", err)
				}

				targetUv := tc.uv
				if tc.overrideRoundtripUv != nil {
					targetUv = *tc.overrideRoundtripUv
				}
				if !reflect.DeepEqual(uv, targetUv) {
					t.Errorf("Expected uv: %v, got: %v", targetUv, uv)
				}
			}
		})
	}
}

// Since we make use of the standard utf.ValidString
// we just do a sanity check to ensure it is used on Marshall/UnMarshal
func TestMarshalUTF8Validation(t *testing.T) {

	uv := UniversalValue{
		// Invalid utf8 byte sequence, string() just casts byte[] verbatim whereas "" causes compile error
		UTF8String: string([]byte{0xc3, 0x28}),
	}

	_, err := MarshalUniversalValue(uv)
	if err == nil {
		t.Error("Expected invalid UTF8 string to raise error")
	}

	inValidASN1UTF8 := asn1.RawValue{
		Tag:   asn1.TagUTF8String,
		Class: asn1.ClassUniversal,
		Bytes: []byte{0xe2, 0x82, 0x28}, // Another out of range utf8 byte sequence
	}

	_, err = UnmarshalUniversalValue(inValidASN1UTF8)
	if err == nil {
		t.Error("Expected invalid UTF8 asn1 value to raise error")
	}
}

func TestIsIA5String(t *testing.T) {
	ia5Strings := []string{
		"test",
		"1234",
		"!@#$",
		" ",
		"",
	}

	for _, ia5String := range ia5Strings {
		err := isIA5String(ia5String)

		if err != nil {
			t.Errorf("Expected IA5 string %q, got: %s", ia5String, err.Error())
		}
	}

	nonIA5Strings := []string{
		"中文",
	}

	for _, nonIA5String := range nonIA5Strings {
		err := isIA5String(nonIA5String)

		if err == nil {
			t.Errorf("Expected non-IA5 string error for %s, got: nil", nonIA5String)
		}
	}
}

func TestIsPrintable(t *testing.T) {
	printableStrings := []string{
		"test",
		"1234",
		"*AA:-)/?",
		" ",
		"",
		"Test*",
		"Test&",
	}

	for _, printableString := range printableStrings {
		isPrintable := isPrintable(printableString)

		if !isPrintable {
			t.Errorf("Expected printable string %q, got: %v", printableString, isPrintable)
		}
	}

	nonPrintableStrings := []string{
		"中文",
		"Test!",
		"Test@",
		"Test#",
		"Test%",
	}

	for _, nonPrintableString := range nonPrintableStrings {
		isPrintable := isPrintable(nonPrintableString)

		if isPrintable {
			t.Errorf("Expected non-printable string %q, got: %v", nonPrintableString, isPrintable)
		}
	}
}
