// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1

package pki

import (
	"fmt"
	"net"
	"strings"
	"testing"

	"github.com/hashicorp/vault/builtin/logical/pki/issuing"
	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestACME_ValidateIdentifiersAgainstRole Verify the ACME order creation
// function verifies somewhat the identifiers that were provided have a
// decent chance of being allowed by the selected role.
func TestACME_ValidateIdentifiersAgainstRole(t *testing.T) {
	b, _ := CreateBackendWithStorage(t)

	tests := []struct {
		name        string
		role        *issuing.RoleEntry
		identifiers []*ACMEIdentifier
		expectErr   bool
	}{
		{
			name:        "verbatim-role-allows-dns-ip",
			role:        issuing.SignVerbatimRole(),
			identifiers: _buildACMEIdentifiers("test.com", "127.0.0.1"),
			expectErr:   false,
		},
		{
			name:        "default-role-does-not-allow-dns",
			role:        buildTestRole(t, nil),
			identifiers: _buildACMEIdentifiers("www.test.com"),
			expectErr:   true,
		},
		{
			name:        "default-role-allows-ip",
			role:        buildTestRole(t, nil),
			identifiers: _buildACMEIdentifiers("192.168.0.1"),
			expectErr:   false,
		},
		{
			name:        "disable-ip-sans-forbids-ip",
			role:        buildTestRole(t, map[string]interface{}{"allow_ip_sans": false}),
			identifiers: _buildACMEIdentifiers("192.168.0.1"),
			expectErr:   true,
		},
		{
			name: "role-no-wildcards-allowed-without",
			role: buildTestRole(t, map[string]interface{}{
				"allow_subdomains":            true,
				"allow_bare_domains":          true,
				"allowed_domains":             []string{"test.com"},
				"allow_wildcard_certificates": false,
			}),
			identifiers: _buildACMEIdentifiers("www.test.com", "test.com"),
			expectErr:   false,
		},
		{
			name: "role-no-wildcards-allowed-with-wildcard",
			role: buildTestRole(t, map[string]interface{}{
				"allow_subdomains":            true,
				"allowed_domains":             []string{"test.com"},
				"allow_wildcard_certificates": false,
			}),
			identifiers: _buildACMEIdentifiers("*.test.com"),
			expectErr:   true,
		},
		{
			name: "role-wildcards-allowed-with-wildcard",
			role: buildTestRole(t, map[string]interface{}{
				"allow_subdomains":            true,
				"allowed_domains":             []string{"test.com"},
				"allow_wildcard_certificates": true,
			}),
			identifiers: _buildACMEIdentifiers("*.test.com"),
			expectErr:   false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := b.validateIdentifiersAgainstRole(tt.role, tt.identifiers)

			if tt.expectErr {
				require.Error(t, err, "validateIdentifiersAgainstRole(%v, %v)", tt.role.ToResponseData(), tt.identifiers)
				// If we did return an error if should be classified as a ErrRejectedIdentifier
				require.ErrorIs(t, err, ErrRejectedIdentifier)
			} else {
				require.NoError(t, err, "validateIdentifiersAgainstRole(%v, %v)", tt.role.ToResponseData(), tt.identifiers)
			}
		})
	}
}

// Test_parseOrderIdentifiers validates we convert ACME requests into proper ACMEIdentifiers
func Test_parseOrderIdentifiers(t *testing.T) {
	tests := []struct {
		name    string
		data    map[string]interface{}
		want    *ACMEIdentifier
		wantErr assert.ErrorAssertionFunc
	}{
		{
			name: "ipv4",
			data: map[string]interface{}{"type": "ip", "value": "192.168.1.1"},
			want: &ACMEIdentifier{
				Type:          ACMEIPIdentifier,
				Value:         "192.168.1.1",
				OriginalValue: "192.168.1.1",
				IsWildcard:    false,
				IsV6IP:        false,
			},
			wantErr: assert.NoError,
		},
		{
			name: "ipv6",
			data: map[string]interface{}{"type": "ip", "value": "2001:0:130F::9C0:876A:130B"},
			want: &ACMEIdentifier{
				Type:          ACMEIPIdentifier,
				Value:         "2001:0:130F::9C0:876A:130B",
				OriginalValue: "2001:0:130F::9C0:876A:130B",
				IsWildcard:    false,
				IsV6IP:        true,
			},
			wantErr: assert.NoError,
		},
		{
			name: "ipv4-in-ipv6",
			data: map[string]interface{}{"type": "ip", "value": "::ffff:192.168.1.1"},
			want: &ACMEIdentifier{
				Type:          ACMEIPIdentifier,
				Value:         "::ffff:192.168.1.1",
				OriginalValue: "::ffff:192.168.1.1",
				IsWildcard:    false,
				IsV6IP:        true,
			},
			wantErr: assert.NoError,
		},
		{
			name: "dns",
			data: map[string]interface{}{"type": "dns", "value": "dadgarcorp.com"},
			want: &ACMEIdentifier{
				Type:          ACMEDNSIdentifier,
				Value:         "dadgarcorp.com",
				OriginalValue: "dadgarcorp.com",
				IsWildcard:    false,
				IsV6IP:        false,
			},
			wantErr: assert.NoError,
		},
		{
			name: "wildcard-dns",
			data: map[string]interface{}{"type": "dns", "value": "*.dadgarcorp.com"},
			want: &ACMEIdentifier{
				Type:          ACMEDNSIdentifier,
				Value:         "dadgarcorp.com",
				OriginalValue: "*.dadgarcorp.com",
				IsWildcard:    true,
				IsV6IP:        false,
			},
			wantErr: assert.NoError,
		},
		{
			name:    "ipv6-with-zone", // This is debatable if we should strip or fail
			data:    map[string]interface{}{"type": "ip", "value": "fe80::1cc0:3e8c:119f:c2e1%ens18"},
			wantErr: ErrorContains("IPv6 identifiers with zone information are not allowed"),
		},
		{
			name:    "bad-dns-wildcard",
			data:    map[string]interface{}{"type": "dns", "value": "*192.168.1.1"},
			wantErr: ErrorContains("invalid wildcard"),
		},
		{
			name:    "ip-in-dns",
			data:    map[string]interface{}{"type": "dns", "value": "192.168.1.1"},
			wantErr: ErrorContains("parsed OK as IP address"),
		},
		{
			name:    "empty-identifiers",
			data:    nil,
			wantErr: ErrorContains("no parsed identifiers were found"),
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			identifiers := map[string]interface{}{"identifiers": []interface{}{}}
			if tt.data != nil {
				identifiers["identifiers"] = append(identifiers["identifiers"].([]interface{}), tt.data)
			}
			got, err := parseOrderIdentifiers(identifiers)
			if !tt.wantErr(t, err, fmt.Sprintf("parseOrderIdentifiers(%v)", tt.data)) {
				return
			} else if err != nil {
				// If we passed the test above and an error was set no point in testing below
				return
			}

			require.Len(t, got, 1, "expected a single return value")
			acmeId := got[0]
			require.Equal(t, tt.want.Type, acmeId.Type)
			require.Equal(t, tt.want.Value, acmeId.Value)
			require.Equal(t, tt.want.OriginalValue, acmeId.OriginalValue)
			require.Equal(t, tt.want.IsWildcard, acmeId.IsWildcard)
			require.Equal(t, tt.want.IsV6IP, acmeId.IsV6IP)
		})
	}
}

func ErrorContains(errMsg string) assert.ErrorAssertionFunc {
	return func(t assert.TestingT, err error, i ...interface{}) bool {
		if err == nil {
			return assert.Fail(t, "expected error got none", i...)
		}

		if !strings.Contains(err.Error(), errMsg) {
			return assert.Fail(t, fmt.Sprintf("error did not contain '%s':\n%+v", errMsg, err), i...)
		}

		return true
	}
}

func _buildACMEIdentifiers(values ...string) []*ACMEIdentifier {
	var identifiers []*ACMEIdentifier

	for _, value := range values {
		identifiers = append(identifiers, _buildACMEIdentifier(value))
	}

	return identifiers
}

func _buildACMEIdentifier(val string) *ACMEIdentifier {
	ip := net.ParseIP(val)
	if ip == nil {
		identifier := &ACMEIdentifier{Type: "dns", Value: val, OriginalValue: val, IsWildcard: false}
		_, _, _ = identifier.MaybeParseWildcard()
		return identifier
	}

	return &ACMEIdentifier{Type: "ip", Value: val, OriginalValue: val, IsWildcard: false}
}

// Easily allow tests to create valid roles with proper defaults, since we don't have an easy
// way to generate roles with proper defaults, go through the createRole handler with the handlers
// field data so we pickup all the defaults specified there.
func buildTestRole(t *testing.T, config map[string]interface{}) *issuing.RoleEntry {
	b, s := CreateBackendWithStorage(t)

	path := pathRoles(b)
	fields := path.Fields
	if config == nil {
		config = map[string]interface{}{}
	}

	if _, exists := config["name"]; !exists {
		config["name"] = genUuid()
	}

	_, err := b.pathRoleCreate(ctx, &logical.Request{Storage: s}, &framework.FieldData{Raw: config, Schema: fields})
	require.NoError(t, err, "failed generating role with config %v", config)

	role, err := b.GetRole(ctx, s, config["name"].(string))
	require.NoError(t, err, "failed loading stored role")

	return role
}
