package config

import (
	"bytes"
	"fmt"
	"sort"
	"strings"
)

// TestString is a Stringer-like function that outputs a string that can
// be used to easily compare multiple Config structures in unit tests.
//
// This function has no practical use outside of unit tests and debugging.
func (c *Config) TestString() string {
	if c == nil {
		return "<nil config>"
	}

	var buf bytes.Buffer
	if len(c.Modules) > 0 {
		buf.WriteString("Modules:\n\n")
		buf.WriteString(modulesStr(c.Modules))
		buf.WriteString("\n\n")
	}

	if len(c.Variables) > 0 {
		buf.WriteString("Variables:\n\n")
		buf.WriteString(variablesStr(c.Variables))
		buf.WriteString("\n\n")
	}

	if len(c.ProviderConfigs) > 0 {
		buf.WriteString("Provider Configs:\n\n")
		buf.WriteString(providerConfigsStr(c.ProviderConfigs))
		buf.WriteString("\n\n")
	}

	if len(c.Resources) > 0 {
		buf.WriteString("Resources:\n\n")
		buf.WriteString(resourcesStr(c.Resources))
		buf.WriteString("\n\n")
	}

	if len(c.Outputs) > 0 {
		buf.WriteString("Outputs:\n\n")
		buf.WriteString(outputsStr(c.Outputs))
		buf.WriteString("\n")
	}

	return strings.TrimSpace(buf.String())
}

func terraformStr(t *Terraform) string {
	result := ""

	if b := t.Backend; b != nil {
		result += fmt.Sprintf("backend (%s)\n", b.Type)

		keys := make([]string, 0, len(b.RawConfig.Raw))
		for k, _ := range b.RawConfig.Raw {
			keys = append(keys, k)
		}
		sort.Strings(keys)

		for _, k := range keys {
			result += fmt.Sprintf("  %s\n", k)
		}
	}

	return strings.TrimSpace(result)
}

func modulesStr(ms []*Module) string {
	result := ""
	order := make([]int, 0, len(ms))
	ks := make([]string, 0, len(ms))
	mapping := make(map[string]int)
	for i, m := range ms {
		k := m.Id()
		ks = append(ks, k)
		mapping[k] = i
	}
	sort.Strings(ks)
	for _, k := range ks {
		order = append(order, mapping[k])
	}

	for _, i := range order {
		m := ms[i]
		result += fmt.Sprintf("%s\n", m.Id())

		ks := make([]string, 0, len(m.RawConfig.Raw))
		for k, _ := range m.RawConfig.Raw {
			ks = append(ks, k)
		}
		sort.Strings(ks)

		result += fmt.Sprintf("  source = %s\n", m.Source)

		for _, k := range ks {
			result += fmt.Sprintf("  %s\n", k)
		}
	}

	return strings.TrimSpace(result)
}

func outputsStr(os []*Output) string {
	ns := make([]string, 0, len(os))
	m := make(map[string]*Output)
	for _, o := range os {
		ns = append(ns, o.Name)
		m[o.Name] = o
	}
	sort.Strings(ns)

	result := ""
	for _, n := range ns {
		o := m[n]

		result += fmt.Sprintf("%s\n", n)

		if len(o.DependsOn) > 0 {
			result += fmt.Sprintf("  dependsOn\n")
			for _, d := range o.DependsOn {
				result += fmt.Sprintf("    %s\n", d)
			}
		}

		if len(o.RawConfig.Variables) > 0 {
			result += fmt.Sprintf("  vars\n")
			for _, rawV := range o.RawConfig.Variables {
				kind := "unknown"
				str := rawV.FullKey()

				switch rawV.(type) {
				case *ResourceVariable:
					kind = "resource"
				case *UserVariable:
					kind = "user"
				}

				result += fmt.Sprintf("    %s: %s\n", kind, str)
			}
		}

		if o.Description != "" {
			result += fmt.Sprintf("  description\n    %s\n", o.Description)
		}
	}

	return strings.TrimSpace(result)
}

func localsStr(ls []*Local) string {
	ns := make([]string, 0, len(ls))
	m := make(map[string]*Local)
	for _, l := range ls {
		ns = append(ns, l.Name)
		m[l.Name] = l
	}
	sort.Strings(ns)

	result := ""
	for _, n := range ns {
		l := m[n]

		result += fmt.Sprintf("%s\n", n)

		if len(l.RawConfig.Variables) > 0 {
			result += fmt.Sprintf("  vars\n")
			for _, rawV := range l.RawConfig.Variables {
				kind := "unknown"
				str := rawV.FullKey()

				switch rawV.(type) {
				case *ResourceVariable:
					kind = "resource"
				case *UserVariable:
					kind = "user"
				}

				result += fmt.Sprintf("    %s: %s\n", kind, str)
			}
		}
	}

	return strings.TrimSpace(result)
}

// This helper turns a provider configs field into a deterministic
// string value for comparison in tests.
func providerConfigsStr(pcs []*ProviderConfig) string {
	result := ""

	ns := make([]string, 0, len(pcs))
	m := make(map[string]*ProviderConfig)
	for _, n := range pcs {
		ns = append(ns, n.Name)
		m[n.Name] = n
	}
	sort.Strings(ns)

	for _, n := range ns {
		pc := m[n]

		result += fmt.Sprintf("%s\n", n)

		keys := make([]string, 0, len(pc.RawConfig.Raw))
		for k, _ := range pc.RawConfig.Raw {
			keys = append(keys, k)
		}
		sort.Strings(keys)

		for _, k := range keys {
			result += fmt.Sprintf("  %s\n", k)
		}

		if len(pc.RawConfig.Variables) > 0 {
			result += fmt.Sprintf("  vars\n")
			for _, rawV := range pc.RawConfig.Variables {
				kind := "unknown"
				str := rawV.FullKey()

				switch rawV.(type) {
				case *ResourceVariable:
					kind = "resource"
				case *UserVariable:
					kind = "user"
				}

				result += fmt.Sprintf("    %s: %s\n", kind, str)
			}
		}
	}

	return strings.TrimSpace(result)
}

// This helper turns a resources field into a deterministic
// string value for comparison in tests.
func resourcesStr(rs []*Resource) string {
	result := ""
	order := make([]int, 0, len(rs))
	ks := make([]string, 0, len(rs))
	mapping := make(map[string]int)
	for i, r := range rs {
		k := r.Id()
		ks = append(ks, k)
		mapping[k] = i
	}
	sort.Strings(ks)
	for _, k := range ks {
		order = append(order, mapping[k])
	}

	for _, i := range order {
		r := rs[i]
		result += fmt.Sprintf(
			"%s (x%s)\n",
			r.Id(),
			r.RawCount.Value())

		ks := make([]string, 0, len(r.RawConfig.Raw))
		for k, _ := range r.RawConfig.Raw {
			ks = append(ks, k)
		}
		sort.Strings(ks)

		for _, k := range ks {
			result += fmt.Sprintf("  %s\n", k)
		}

		if len(r.Provisioners) > 0 {
			result += fmt.Sprintf("  provisioners\n")
			for _, p := range r.Provisioners {
				when := ""
				if p.When != ProvisionerWhenCreate {
					when = fmt.Sprintf(" (%s)", p.When.String())
				}

				result += fmt.Sprintf("    %s%s\n", p.Type, when)

				if p.OnFailure != ProvisionerOnFailureFail {
					result += fmt.Sprintf("      on_failure = %s\n", p.OnFailure.String())
				}

				ks := make([]string, 0, len(p.RawConfig.Raw))
				for k, _ := range p.RawConfig.Raw {
					ks = append(ks, k)
				}
				sort.Strings(ks)

				for _, k := range ks {
					result += fmt.Sprintf("      %s\n", k)
				}
			}
		}

		if len(r.DependsOn) > 0 {
			result += fmt.Sprintf("  dependsOn\n")
			for _, d := range r.DependsOn {
				result += fmt.Sprintf("    %s\n", d)
			}
		}

		if len(r.RawConfig.Variables) > 0 {
			result += fmt.Sprintf("  vars\n")

			ks := make([]string, 0, len(r.RawConfig.Variables))
			for k, _ := range r.RawConfig.Variables {
				ks = append(ks, k)
			}
			sort.Strings(ks)

			for _, k := range ks {
				rawV := r.RawConfig.Variables[k]
				kind := "unknown"
				str := rawV.FullKey()

				switch rawV.(type) {
				case *ResourceVariable:
					kind = "resource"
				case *UserVariable:
					kind = "user"
				}

				result += fmt.Sprintf("    %s: %s\n", kind, str)
			}
		}
	}

	return strings.TrimSpace(result)
}

// This helper turns a variables field into a deterministic
// string value for comparison in tests.
func variablesStr(vs []*Variable) string {
	result := ""
	ks := make([]string, 0, len(vs))
	m := make(map[string]*Variable)
	for _, v := range vs {
		ks = append(ks, v.Name)
		m[v.Name] = v
	}
	sort.Strings(ks)

	for _, k := range ks {
		v := m[k]

		required := ""
		if v.Required() {
			required = " (required)"
		}

		declaredType := ""
		if v.DeclaredType != "" {
			declaredType = fmt.Sprintf(" (%s)", v.DeclaredType)
		}

		if v.Default == nil || v.Default == "" {
			v.Default = "<>"
		}
		if v.Description == "" {
			v.Description = "<>"
		}

		result += fmt.Sprintf(
			"%s%s%s\n  %v\n  %s\n",
			k,
			required,
			declaredType,
			v.Default,
			v.Description)
	}

	return strings.TrimSpace(result)
}
