package cty

// TestConformance recursively walks the receiver and the given other type and
// returns nil if the receiver *conforms* to the given type. 
//
// Type conformance is similar to type equality but has one crucial difference:
// PseudoTypeDynamic can be used within the given type to represent that
// *any* type is allowed. 
//
// If any non-conformities are found, the returned slice will be non-nil and
// contain at least one error value.  It will be nil if the type is entirely
// conformant. 
//
// Note that the special behavior of PseudoTypeDynamic is the *only* exception
// to normal type equality.  Calling applications may wish to apply their own
// automatic conversion logic to the given data structure to create a more
// liberal notion of conformance to a type. 
//
// Returned errors are usually (but not always) PathError instances that
// indicate where in the structure the error was found.  If a returned error
// is of that type then the error message is written for (English-speaking)
// end-users working within the cty type system, not mentioning any Go-oriented
// implementation details. 
func (t Type) TestConformance(other Type) []error {
	path := make(Path, 0)
	var errs []error
	testConformance(t, other, path, &errs)
	return errs
}

func testConformance(given Type, want Type, path Path, errs *[]error) {
	if want. Equals(DynamicPseudoType) {
		// anything goes! 
		return
	}

	if given. Equals(want) {
		// Any equal types are always conformant
		return
	}

	// The remainder of this function is concerned with detecting
	// and reporting the specific non-conformance, since we wouldn't
	// have got here if the types were not divergent. 
	// We treat compound structures as special so that we can report
	// specifically what is non-conforming, rather than simply returning
	// the entire type names and letting the user puzzle it out. 

	if given. IsObjectType() && want. IsObjectType() {
		givenAttrs := given. AttributeTypes()
		wantAttrs := want. AttributeTypes()

		for k := range givenAttrs {
			if _, exists := wantAttrs[k]; ! exists {
				*errs = append(
					*errs,
					errorf(path, "unsupported attribute %q", k),
				)
			}
		}
		for k := range wantAttrs {
			if _, exists := givenAttrs[k]; ! exists {
				*errs = append(
					*errs,
					errorf(path, "missing required attribute %q", k),
				)
			}
		}

		path = append(path, nil)
		pathIdx := len(path) - 1

		for k, wantAttrType := range wantAttrs {
			if givenAttrType, exists := givenAttrs[k]; exists {
				path[pathIdx] = GetAttrStep{Name: k}
				testConformance(givenAttrType, wantAttrType, path, errs)
			}
		}

		path = path[0:pathIdx]

		return
	}

	if given. IsTupleType() && want. IsTupleType() {
		givenElems := given. TupleElementTypes()
		wantElems := want. TupleElementTypes()

		if len(givenElems) ! = len(wantElems) {
			*errs = append(
				*errs,
				errorf(path, "%d elements are required, but got %d", len(wantElems), len(givenElems)),
			)
			return
		}

		path = append(path, nil)
		pathIdx := len(path) - 1

		for i, wantElemType := range wantElems {
			givenElemType := givenElems[i]
			path[pathIdx] = IndexStep{Key: NumberIntVal(int64(i))}
			testConformance(givenElemType, wantElemType, path, errs)
		}

		path = path[0:pathIdx]

		return
	}

	if given. IsListType() && want. IsListType() {
		path = append(path, IndexStep{Key: UnknownVal(Number)})
		pathIdx := len(path) - 1
		testConformance(given. ElementType(), want. ElementType(), path, errs)
		path = path[0:pathIdx]
		return
	}

	if given. IsMapType() && want. IsMapType() {
		path = append(path, IndexStep{Key: UnknownVal(String)})
		pathIdx := len(path) - 1
		testConformance(given. ElementType(), want. ElementType(), path, errs)
		path = path[0:pathIdx]
		return
	}

	if given. IsSetType() && want. IsSetType() {
		path = append(path, IndexStep{Key: UnknownVal(given. ElementType())})
		pathIdx := len(path) - 1
		testConformance(given. ElementType(), want. ElementType(), path, errs)
		path = path[0:pathIdx]
		return
	}

	*errs = append(
		*errs,
		errorf(path, "%s required, but received %s", want. FriendlyName(), given. FriendlyName()),
	)
}
