package bpdoc

import (
	"fmt"
	"reflect"
	"testing"
)

type propInfo struct {
	name string
	typ  string
}

type parentProps struct {
	A string

	Child *childProps

	Mutated *mutatedProps `blueprint:"mutated"`
}

type childProps struct {
	B int

	Child *grandchildProps
}

type grandchildProps struct {
	C bool
}

type mutatedProps struct {
	D int
}

func TestNestedPropertyStructs(t *testing.T) {
	parent := parentProps{Child: &childProps{Child: &grandchildProps{}}, Mutated: &mutatedProps{}}

	allStructs := nestedPropertyStructs(reflect.ValueOf(parent))

	// mutated shouldn't be found because it's a mutated property.
	expected := []string{"child", "child.child"}
	if len(allStructs) != len(expected) {
		t.Fatalf("expected %d structs, got %d, all entries: %v",
			len(expected), len(allStructs), allStructs)
	}
	got := []string{}
	for _, s := range allStructs {
		got = append(got, s.nestPoint)
	}

	if !reflect.DeepEqual(got, expected) {
		t.Errorf("Expected nested properties:\n\t %q,\n but got\n\t %q", expected, got)
	}
}

func TestAllPackages(t *testing.T) {
	packages, err := AllPackages(pkgFiles, moduleTypeNameFactories, moduleTypeNamePropertyStructs)
	if err != nil {
		t.Fatalf("expected nil error for AllPackages(%v, %v, %v), got %s", pkgFiles, moduleTypeNameFactories, moduleTypeNamePropertyStructs, err)
	}

	if numPackages := len(packages); numPackages != 1 {
		t.Errorf("Expected %d package, got %d packages %v instead", len(pkgFiles), numPackages, packages)
	}

	pkg := packages[0]

	expectedProps := map[string][]propInfo{
		"bar": []propInfo{
			propInfo{
				name: "a",
				typ:  "string",
			},
			propInfo{
				name: "nested",
				typ:  "",
			},
			propInfo{
				name: "nested.c",
				typ:  "string",
			},
			propInfo{
				name: "nested_struct",
				typ:  "structToNest",
			},
			propInfo{
				name: "nested_struct.e",
				typ:  "string",
			},
			propInfo{
				name: "struct_has_embed",
				typ:  "StructWithEmbedded",
			},
			propInfo{
				name: "struct_has_embed.nested_in_embedded",
				typ:  "structToNest",
			},
			propInfo{
				name: "struct_has_embed.nested_in_embedded.e",
				typ:  "string",
			},
			propInfo{
				name: "struct_has_embed.f",
				typ:  "string",
			},
			propInfo{
				name: "list_of_ints",
				typ:  "list of int",
			},
			propInfo{
				name: "list_of_nested",
				typ:  "list of structToNest",
			},
			propInfo{
				name: "configurable_bool",
				typ:  "configurable bool",
			},
			propInfo{
				name: "nested_in_other_embedded",
				typ:  "otherStructToNest",
			},
			propInfo{
				name: "nested_in_other_embedded.g",
				typ:  "string",
			},
			propInfo{
				name: "h",
				typ:  "string",
			},
		},
		"foo": []propInfo{
			propInfo{
				name: "a",
				typ:  "string",
			},
		},
	}

	for _, m := range pkg.ModuleTypes {
		foundProps := []propInfo{}

		for _, p := range m.PropertyStructs {
			nestedProps, errs := findAllProperties("", p.Properties)
			foundProps = append(foundProps, nestedProps...)
			for _, err := range errs {
				t.Errorf("%s", err)
			}
		}
		if wanted, ok := expectedProps[m.Name]; ok {
			if !reflect.DeepEqual(foundProps, wanted) {
				t.Errorf("For %s, expected\n\t %q,\nbut got\n\t %q", m.Name, wanted, foundProps)
			}
		}
	}
}

func findAllProperties(prefix string, properties []Property) ([]propInfo, []error) {
	foundProps := []propInfo{}
	errs := []error{}
	for _, p := range properties {
		prop := propInfo{
			name: prefix + p.Name,
			typ:  p.Type,
		}
		foundProps = append(foundProps, prop)
		if hasTag(p.Tag, "blueprint", "mutated") {
			err := fmt.Errorf("Property %s has `blueprint:\"mutated\" tag but should have been excluded.", p.Name)
			errs = append(errs, err)
		}

		nestedProps, nestedErrs := findAllProperties(prefix+p.Name+".", p.Properties)
		foundProps = append(foundProps, nestedProps...)
		errs = append(errs, nestedErrs...)
	}
	return foundProps, errs
}
