// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package configschema

import (
	"testing"

	"github.com/zclconf/go-cty/cty"
)

func TestBlockInternalValidate(t *testing.T) {
	tests := map[string]struct {
		Block *Block
		Errs  []string
	}{
		"empty": {
			&Block{},
			[]string{},
		},
		"valid": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.String,
						Required: true,
					},
					"bar": {
						Type:     cty.String,
						Optional: true,
					},
					"baz": {
						Type:     cty.String,
						Computed: true,
					},
					"baz_maybe": {
						Type:     cty.String,
						Optional: true,
						Computed: true,
					},
				},
				BlockTypes: map[string]*NestedBlock{
					"single": {
						Nesting: NestingSingle,
						Block:   Block{},
					},
					"single_required": {
						Nesting:  NestingSingle,
						Block:    Block{},
						MinItems: 1,
						MaxItems: 1,
					},
					"list": {
						Nesting: NestingList,
						Block:   Block{},
					},
					"list_required": {
						Nesting:  NestingList,
						Block:    Block{},
						MinItems: 1,
					},
					"set": {
						Nesting: NestingSet,
						Block:   Block{},
					},
					"set_required": {
						Nesting:  NestingSet,
						Block:    Block{},
						MinItems: 1,
					},
					"map": {
						Nesting: NestingMap,
						Block:   Block{},
					},
				},
			},
			[]string{},
		},
		"attribute with no flags set": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type: cty.String,
					},
				},
			},
			[]string{"foo: must set Optional, Required or Computed"},
		},
		"attribute required and optional": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.String,
						Required: true,
						Optional: true,
					},
				},
			},
			[]string{"foo: cannot set both Optional and Required"},
		},
		"attribute required and computed": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.String,
						Required: true,
						Computed: true,
					},
				},
			},
			[]string{"foo: cannot set both Computed and Required"},
		},
		"attribute optional and computed": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
						Computed: true,
					},
				},
			},
			[]string{},
		},
		"attribute with deprecation message but not deprecated": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:               cty.String,
						Optional:           true,
						DeprecationMessage: "use bar instead",
					},
				},
			},
			[]string{"foo: DeprecationMessage must not be set when Deprecated is false"},
		},
		"attribute with missing type": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Optional: true,
					},
				},
			},
			[]string{"foo: either Type or NestedType must be defined"},
		},
		/* FIXME: This caused errors when applied to existing providers (oci)
		and cannot be enforced without coordination.

		"attribute with invalid name": {&Block{Attributes:
		    map[string]*Attribute{"fooBar": {Type:     cty.String, Optional:
		    true,
		            },
		        },
		    },
		    []string{"fooBar: name may contain only lowercase letters, digits and underscores"},
		},
		*/
		"attribute with invalid NestedType attribute": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						NestedType: &Object{
							Nesting: NestingSingle,
							Attributes: map[string]*Attribute{
								"foo": {
									Type:     cty.String,
									Required: true,
									Optional: true,
								},
							},
						},
						Optional: true,
					},
				},
			},
			[]string{"foo: cannot set both Optional and Required"},
		},
		"block type with invalid name": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"fooBar": {
						Nesting: NestingSingle,
					},
				},
			},
			[]string{"fooBar: name may contain only lowercase letters, digits and underscores"},
		},
		"colliding names": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
					},
				},
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Nesting: NestingSingle,
					},
				},
			},
			[]string{"foo: name defined as both attribute and child block type"},
		},
		"nested block with badness": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"bad": {
						Nesting: NestingSingle,
						Block: Block{
							Attributes: map[string]*Attribute{
								"nested_bad": {
									Type:     cty.String,
									Required: true,
									Optional: true,
								},
							},
						},
					},
				},
			},
			[]string{"bad.nested_bad: cannot set both Optional and Required"},
		},
		"nested block with deprecation message but not deprecated": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"bad": {
						Nesting: NestingSingle,
						Block: Block{
							DeprecationMessage: "use good instead",
						},
					},
				},
			},
			[]string{"bad: DeprecationMessage must not be set when Deprecated is false"},
		},
		"top-level block with deprecation message but not deprecated": {
			&Block{
				DeprecationMessage: "use another resource instead",
			},
			[]string{"top-level block: DeprecationMessage must not be set when Deprecated is false"},
		},
		"nested list block with dynamically-typed attribute": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"bad": {
						Nesting: NestingList,
						Block: Block{
							Attributes: map[string]*Attribute{
								"nested_bad": {
									Type:     cty.DynamicPseudoType,
									Optional: true,
								},
							},
						},
					},
				},
			},
			[]string{},
		},
		"nested set block with dynamically-typed attribute": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"bad": {
						Nesting: NestingSet,
						Block: Block{
							Attributes: map[string]*Attribute{
								"nested_bad": {
									Type:     cty.DynamicPseudoType,
									Optional: true,
								},
							},
						},
					},
				},
			},
			[]string{"bad: NestingSet blocks may not contain attributes of cty.DynamicPseudoType"},
		},
		"nil": {
			nil,
			[]string{"top-level block schema is nil"},
		},
		"nil attr": {
			&Block{
				Attributes: map[string]*Attribute{
					"bad": nil,
				},
			},
			[]string{"bad: attribute schema is nil"},
		},
		"nil block type": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"bad": nil,
				},
			},
			[]string{"bad: block schema is nil"},
		},
		"nested set block with write-only attribute": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"bad": {
						Nesting: NestingSet,
						Block: Block{
							Attributes: map[string]*Attribute{
								"wo": {
									Type:      cty.String,
									Optional:  true,
									WriteOnly: true,
								},
							},
						},
					},
				},
			},
			[]string{"bad: NestingSet blocks may not contain WriteOnly attributes"},
		},
		"nested set attributes with write-only attribute": {
			&Block{
				Attributes: map[string]*Attribute{
					"bad": {
						NestedType: &Object{
							Attributes: map[string]*Attribute{
								"wo": {
									Type:      cty.String,
									Optional:  true,
									WriteOnly: true,
								},
							},
							Nesting: NestingSet,
						},
						Optional: true,
					},
				},
			},
			[]string{"bad: NestingSet attributes may not contain WriteOnly attributes"},
		},

		"nested computed block with non-computed attr": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"partial": &NestedBlock{
						Block: Block{
							Computed: true,
							Attributes: map[string]*Attribute{
								"required": {Type: cty.String, Required: true},
								"computed": {Type: cty.String, Computed: true},
							},

							BlockTypes: map[string]*NestedBlock{
								"nested": &NestedBlock{
									Block: Block{
										Attributes: map[string]*Attribute{
											"optional": {Type: cty.String, Optional: true},
										},
									},
									Nesting: NestingList,
								},
							},
						},
						Nesting: NestingList,
					},
				},
			},
			[]string{
				`partial.required: all attributes within computed blocks must also be computed`,
				`partial.nested: all nested blocks within computed blocks must also be computed`,
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			errs := joinedErrors(test.Block.InternalValidate())
			if got, want := len(errs), len(test.Errs); got != want {
				t.Errorf("wrong number of errors %d; want %d", got, want)
				for _, err := range errs {
					t.Logf("- %s", err.Error())
				}
			} else {
				if len(errs) > 0 {
					for i := range errs {
						if errs[i].Error() != test.Errs[i] {
							t.Errorf("wrong error: got %s, want %s", errs[i].Error(), test.Errs[i])
						}
					}
				}
			}
		})
	}
}

func TestObjectInternalValidate(t *testing.T) {
	tests := map[string]struct {
		Object *Object
		Errs   []string
	}{
		"empty": {
			&Object{Nesting: NestingSingle},
			[]string{},
		},
		"valid": {
			&Object{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.String,
						Required: true,
					},
					"bar": {
						Type:     cty.String,
						Optional: true,
					},
				},
				Nesting: NestingSingle,
			},
			[]string{},
		},
		"nil": {
			&Object{
				Attributes: map[string]*Attribute{
					"foo": nil,
				},
				Nesting: NestingSingle,
			},
			[]string{"foo: attribute schema is nil"},
		},
		"attribute with no flags set": {
			&Object{
				Attributes: map[string]*Attribute{
					"foo": {
						Type: cty.String,
					},
				},
				Nesting: NestingSingle,
			},
			[]string{"foo: must set Optional, Required or Computed"},
		},
		"attribute required and optional": {
			&Object{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.String,
						Required: true,
						Optional: true,
					},
				},
				Nesting: NestingSingle,
			},
			[]string{"foo: cannot set both Optional and Required"},
		},
		"attribute with missing type": {
			&Object{
				Attributes: map[string]*Attribute{
					"foo": {
						Optional: true,
					},
				},
				Nesting: NestingSingle,
			},
			[]string{"foo: either Type or NestedType must be defined"},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			errs := joinedErrors(test.Object.InternalValidate())
			if got, want := len(errs), len(test.Errs); got != want {
				t.Errorf("wrong number of errors %d; want %d", got, want)
				for _, err := range errs {
					t.Logf("- %s", err.Error())
				}
			} else {
				if len(errs) > 0 {
					for i := range errs {
						if errs[i].Error() != test.Errs[i] {
							t.Errorf("wrong error: got %s, want %s", errs[i].Error(), test.Errs[i])
						}
					}
				}
			}
		})
	}
}

func joinedErrors(err error) []error {
	if err == nil {
		return nil
	}
	// This interface is implemented by the result of errors.Join when
	// multiple errors are present.
	type Unwrapper interface {
		Unwrap() []error
	}

	var res []error

	switch terr := err.(type) {
	case Unwrapper:
		for _, err := range terr.Unwrap() {
			res = append(res, joinedErrors(err)...)
		}
	default:
		return []error{err}
	}
	return res
}
