package parser

import (
	"bytes"
	"io/fs"
	"log/slog"
	"os"
	"path/filepath"
	"slices"
	"sort"
	"testing"
	"testing/fstest"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/zclconf/go-cty/cty"

	"github.com/aquasecurity/trivy/internal/testutil"
	"github.com/aquasecurity/trivy/pkg/iac/terraform"
	"github.com/aquasecurity/trivy/pkg/log"
	"github.com/aquasecurity/trivy/pkg/mapfs"
	"github.com/aquasecurity/trivy/pkg/set"
)

func Test_BasicParsing(t *testing.T) {

	fs := testutil.CreateFS(map[string]string{
		"test.tf": `

locals {
	proxy = var.cats_mother
}

variable "cats_mother" {
	default = "boots"
}

provider "cats" {

}

moved {

}

action "aws_lambda_invoke" "example" {
  config {
    function_name = "123456789012:function:my-function:1"
    payload = jsonencode({
      key1 = "value1"
      key2 = "value2"
    })
  }
}


import {
  to = cats_cat.mittens
  id = "mittens"
}

resource "cats_cat" "mittens" {
	name = "mittens"
	special = true
}

resource "cats_kitten" "the-great-destroyer" {
	name = "the great destroyer"
	parent = cats_cat.mittens.name
}

data "cats_cat" "the-cats-mother" {
	name = local.proxy
}

check "cats_mittens_is_special" {
  data "cats_cat" "mittens" {
    name = "mittens"
  }

  assert {
    condition = data.cats_cat.mittens.special == true
    error_message = "${data.cats_cat.mittens.name} must be special"
  }
}

`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	blocks := modules[0].GetBlocks()

	// variable
	variables := blocks.OfType("variable")
	require.Len(t, variables, 1)
	assert.Equal(t, "variable", variables[0].Type())
	require.Len(t, variables[0].Labels(), 1)
	assert.Equal(t, "cats_mother", variables[0].TypeLabel())
	defaultVal := variables[0].GetAttribute("default")
	require.NotNil(t, defaultVal)
	assert.Equal(t, cty.String, defaultVal.Value().Type())
	assert.Equal(t, "boots", defaultVal.Value().AsString())

	// provider
	providerBlocks := blocks.OfType("provider")
	require.Len(t, providerBlocks, 1)
	assert.Equal(t, "provider", providerBlocks[0].Type())
	require.Len(t, providerBlocks[0].Labels(), 1)
	assert.Equal(t, "cats", providerBlocks[0].TypeLabel())

	// resources
	resourceBlocks := blocks.OfType("resource")

	sort.Slice(resourceBlocks, func(i, j int) bool {
		return resourceBlocks[i].TypeLabel() < resourceBlocks[j].TypeLabel()
	})

	require.Len(t, resourceBlocks, 2)
	require.Len(t, resourceBlocks[0].Labels(), 2)

	assert.Equal(t, "resource", resourceBlocks[0].Type())
	assert.Equal(t, "cats_cat", resourceBlocks[0].TypeLabel())
	assert.Equal(t, "mittens", resourceBlocks[0].NameLabel())

	assert.Equal(t, "mittens", resourceBlocks[0].GetAttribute("name").Value().AsString())
	assert.True(t, resourceBlocks[0].GetAttribute("special").Value().True())

	assert.Equal(t, "resource", resourceBlocks[1].Type())
	assert.Equal(t, "cats_kitten", resourceBlocks[1].TypeLabel())
	assert.Equal(t, "the great destroyer", resourceBlocks[1].GetAttribute("name").Value().AsString())
	assert.Equal(t, "mittens", resourceBlocks[1].GetAttribute("parent").Value().AsString())

	// import
	importBlocks := blocks.OfType("import")

	assert.Equal(t, "import", importBlocks[0].Type())
	require.NotNil(t, importBlocks[0].GetAttribute("to"))
	assert.Equal(t, "mittens", importBlocks[0].GetAttribute("id").Value().AsString())

	// data
	dataBlocks := blocks.OfType("data")
	require.Len(t, dataBlocks, 1)
	require.Len(t, dataBlocks[0].Labels(), 2)

	assert.Equal(t, "data", dataBlocks[0].Type())
	assert.Equal(t, "cats_cat", dataBlocks[0].TypeLabel())
	assert.Equal(t, "the-cats-mother", dataBlocks[0].NameLabel())

	assert.Equal(t, "boots", dataBlocks[0].GetAttribute("name").Value().AsString())

	// check
	checkBlocks := blocks.OfType("check")
	require.Len(t, checkBlocks, 1)
	require.Len(t, checkBlocks[0].Labels(), 1)

	assert.Equal(t, "check", checkBlocks[0].Type())
	assert.Equal(t, "cats_mittens_is_special", checkBlocks[0].TypeLabel())

	require.NotNil(t, checkBlocks[0].GetBlock("data"))
	require.NotNil(t, checkBlocks[0].GetBlock("assert"))
}

func Test_Modules(t *testing.T) {

	fs := testutil.CreateFS(map[string]string{
		"code/test.tf": `
module "my-mod" {
	source = "../module"
	input = "ok"
}

output "result" {
	value = module.my-mod.mod_result
}
`,
		"module/module.tf": `
variable "input" {
	default = "?"
}

output "mod_result" {
	value = var.input
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "code"))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	require.Len(t, modules, 2)
	rootModule := modules[0]
	childModule := modules[1]

	moduleBlocks := rootModule.GetBlocks().OfType("module")
	require.Len(t, moduleBlocks, 1)

	assert.Equal(t, "module", moduleBlocks[0].Type())
	assert.Equal(t, "module.my-mod", moduleBlocks[0].FullName())
	inputAttr := moduleBlocks[0].GetAttribute("input")
	require.NotNil(t, inputAttr)
	require.Equal(t, cty.String, inputAttr.Value().Type())
	assert.Equal(t, "ok", inputAttr.Value().AsString())

	rootOutputs := rootModule.GetBlocks().OfType("output")
	require.Len(t, rootOutputs, 1)
	assert.Equal(t, "output.result", rootOutputs[0].FullName())
	valAttr := rootOutputs[0].GetAttribute("value")
	require.NotNil(t, valAttr)
	require.Equal(t, cty.String, valAttr.Type())
	assert.Equal(t, "ok", valAttr.Value().AsString())

	childOutputs := childModule.GetBlocks().OfType("output")
	require.Len(t, childOutputs, 1)
	assert.Equal(t, "module.my-mod.output.mod_result", childOutputs[0].FullName())
	childValAttr := childOutputs[0].GetAttribute("value")
	require.NotNil(t, childValAttr)
	require.Equal(t, cty.String, childValAttr.Type())
	assert.Equal(t, "ok", childValAttr.Value().AsString())

}

func Test_NestedParentModule(t *testing.T) {

	fs := testutil.CreateFS(map[string]string{
		"code/test.tf": `
module "my-mod" {
	source = "../."
	input = "ok"
}

output "result" {
	value = module.my-mod.mod_result
}
`,
		"root.tf": `
variable "input" {
	default = "?"
}

output "mod_result" {
	value = var.input
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "code"))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	require.Len(t, modules, 2)
	rootModule := modules[0]
	childModule := modules[1]

	moduleBlocks := rootModule.GetBlocks().OfType("module")
	require.Len(t, moduleBlocks, 1)

	assert.Equal(t, "module", moduleBlocks[0].Type())
	assert.Equal(t, "module.my-mod", moduleBlocks[0].FullName())
	inputAttr := moduleBlocks[0].GetAttribute("input")
	require.NotNil(t, inputAttr)
	require.Equal(t, cty.String, inputAttr.Value().Type())
	assert.Equal(t, "ok", inputAttr.Value().AsString())

	rootOutputs := rootModule.GetBlocks().OfType("output")
	require.Len(t, rootOutputs, 1)
	assert.Equal(t, "output.result", rootOutputs[0].FullName())
	valAttr := rootOutputs[0].GetAttribute("value")
	require.NotNil(t, valAttr)
	require.Equal(t, cty.String, valAttr.Type())
	assert.Equal(t, "ok", valAttr.Value().AsString())

	childOutputs := childModule.GetBlocks().OfType("output")
	require.Len(t, childOutputs, 1)
	assert.Equal(t, "module.my-mod.output.mod_result", childOutputs[0].FullName())
	childValAttr := childOutputs[0].GetAttribute("value")
	require.NotNil(t, childValAttr)
	require.Equal(t, cty.String, childValAttr.Type())
	assert.Equal(t, "ok", childValAttr.Value().AsString())
}

func Test_UndefinedModuleOutputReference(t *testing.T) {

	fs := testutil.CreateFS(map[string]string{
		"code/test.tf": `
resource "something" "blah" {
	value = module.x.y
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "code"))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	require.Len(t, modules, 1)
	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("something")
	require.Len(t, blocks, 1)
	block := blocks[0]

	attr := block.GetAttribute("value")
	require.NotNil(t, attr)

	assert.False(t, attr.IsResolvable())
}

func Test_UndefinedModuleOutputReferenceInSlice(t *testing.T) {

	fs := testutil.CreateFS(map[string]string{
		"code/test.tf": `
resource "something" "blah" {
	value = ["first", module.x.y, "last"]
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "code"))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	require.Len(t, modules, 1)
	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("something")
	require.Len(t, blocks, 1)
	block := blocks[0]

	attr := block.GetAttribute("value")
	require.NotNil(t, attr)

	assert.True(t, attr.IsResolvable())

	values := attr.AsStringValueSliceOrEmpty()
	require.Len(t, values, 3)

	assert.Equal(t, "first", values[0].Value())
	assert.True(t, values[0].GetMetadata().IsResolvable())

	assert.False(t, values[1].GetMetadata().IsResolvable())

	assert.Equal(t, "last", values[2].Value())
	assert.True(t, values[2].GetMetadata().IsResolvable())
}

func Test_TemplatedSliceValue(t *testing.T) {

	fs := testutil.CreateFS(map[string]string{
		"code/test.tf": `

variable "x" {
	default = "hello"
}

resource "something" "blah" {
	value = ["first", "${var.x}-${var.x}", "last"]
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "code"))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	require.Len(t, modules, 1)
	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("something")
	require.Len(t, blocks, 1)
	block := blocks[0]

	attr := block.GetAttribute("value")
	require.NotNil(t, attr)

	assert.True(t, attr.IsResolvable())

	values := attr.AsStringValueSliceOrEmpty()
	require.Len(t, values, 3)

	assert.Equal(t, "first", values[0].Value())
	assert.True(t, values[0].GetMetadata().IsResolvable())

	assert.Equal(t, "hello-hello", values[1].Value())
	assert.True(t, values[1].GetMetadata().IsResolvable())

	assert.Equal(t, "last", values[2].Value())
	assert.True(t, values[2].GetMetadata().IsResolvable())
}

func Test_SliceOfVars(t *testing.T) {

	fs := testutil.CreateFS(map[string]string{
		"code/test.tf": `

variable "x" {
	default = "1"
}

variable "y" {
	default = "2"
}

resource "something" "blah" {
	value = [var.x, var.y]
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "code"))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	require.Len(t, modules, 1)
	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("something")
	require.Len(t, blocks, 1)
	block := blocks[0]

	attr := block.GetAttribute("value")
	require.NotNil(t, attr)

	assert.True(t, attr.IsResolvable())

	values := attr.AsStringValueSliceOrEmpty()
	require.Len(t, values, 2)

	assert.Equal(t, "1", values[0].Value())
	assert.True(t, values[0].GetMetadata().IsResolvable())

	assert.Equal(t, "2", values[1].Value())
	assert.True(t, values[1].GetMetadata().IsResolvable())
}

func Test_VarSlice(t *testing.T) {

	fs := testutil.CreateFS(map[string]string{
		"code/test.tf": `

variable "x" {
	default = ["a", "b", "c"]
}

resource "something" "blah" {
	value = var.x
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "code"))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	require.Len(t, modules, 1)
	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("something")
	require.Len(t, blocks, 1)
	block := blocks[0]

	attr := block.GetAttribute("value")
	require.NotNil(t, attr)

	assert.True(t, attr.IsResolvable())

	values := attr.AsStringValueSliceOrEmpty()
	require.Len(t, values, 3)

	assert.Equal(t, "a", values[0].Value())
	assert.True(t, values[0].GetMetadata().IsResolvable())

	assert.Equal(t, "b", values[1].Value())
	assert.True(t, values[1].GetMetadata().IsResolvable())

	assert.Equal(t, "c", values[2].Value())
	assert.True(t, values[2].GetMetadata().IsResolvable())
}

func Test_LocalSliceNested(t *testing.T) {

	fs := testutil.CreateFS(map[string]string{
		"code/test.tf": `

variable "x" {
	default = "a"
}

locals {
	y = [var.x, "b", "c"]
}

resource "something" "blah" {
	value = local.y
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "code"))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	require.Len(t, modules, 1)
	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("something")
	require.Len(t, blocks, 1)
	block := blocks[0]

	attr := block.GetAttribute("value")
	require.NotNil(t, attr)

	assert.True(t, attr.IsResolvable())

	values := attr.AsStringValueSliceOrEmpty()
	require.Len(t, values, 3)

	assert.Equal(t, "a", values[0].Value())
	assert.True(t, values[0].GetMetadata().IsResolvable())

	assert.Equal(t, "b", values[1].Value())
	assert.True(t, values[1].GetMetadata().IsResolvable())

	assert.Equal(t, "c", values[2].Value())
	assert.True(t, values[2].GetMetadata().IsResolvable())
}

func Test_FunctionCall(t *testing.T) {

	fs := testutil.CreateFS(map[string]string{
		"code/test.tf": `

variable "x" {
	default = ["a", "b"]
}

resource "something" "blah" {
	value = concat(var.x, ["c"])
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "code"))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	require.Len(t, modules, 1)
	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("something")
	require.Len(t, blocks, 1)
	block := blocks[0]

	attr := block.GetAttribute("value")
	require.NotNil(t, attr)

	assert.True(t, attr.IsResolvable())

	values := attr.AsStringValueSliceOrEmpty()
	require.Len(t, values, 3)

	assert.Equal(t, "a", values[0].Value())
	assert.True(t, values[0].GetMetadata().IsResolvable())

	assert.Equal(t, "b", values[1].Value())
	assert.True(t, values[1].GetMetadata().IsResolvable())

	assert.Equal(t, "c", values[2].Value())
	assert.True(t, values[2].GetMetadata().IsResolvable())
}

func Test_NullDefaultValueForVar(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"test.tf": `
variable "bucket_name" {
  type    = string
  default = null
}

resource "aws_s3_bucket" "default" {
  bucket = var.bucket_name != null ? var.bucket_name : "default"
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	require.Len(t, modules, 1)

	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("aws_s3_bucket")
	require.Len(t, blocks, 1)
	block := blocks[0]

	attr := block.GetAttribute("bucket")
	require.NotNil(t, attr)
	assert.Equal(t, "default", attr.Value().AsString())
}

func Test_MultipleInstancesOfSameResource(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"test.tf": `

resource "aws_kms_key" "key1" {
	description         = "Key #1"
	enable_key_rotation = true
}

resource "aws_kms_key" "key2" {
	description         = "Key #2"
	enable_key_rotation = true
}

resource "aws_s3_bucket" "this" {
	bucket        = "test"
  }


resource "aws_s3_bucket_server_side_encryption_configuration" "this1" {
	bucket = aws_s3_bucket.this.id
  
	rule {
	  apply_server_side_encryption_by_default {
		kms_master_key_id = aws_kms_key.key1.arn
		sse_algorithm     = "aws:kms"
	  }
	}
}

resource "aws_s3_bucket_server_side_encryption_configuration" "this2" {
	bucket = aws_s3_bucket.this.id
  
	rule {
	  apply_server_side_encryption_by_default {
		kms_master_key_id = aws_kms_key.key2.arn
		sse_algorithm     = "aws:kms"
	  }
	}
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	assert.Len(t, modules, 1)

	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("aws_s3_bucket_server_side_encryption_configuration")
	assert.Len(t, blocks, 2)

	for _, block := range blocks {
		attr, parent := block.GetNestedAttribute("rule.apply_server_side_encryption_by_default.kms_master_key_id")
		assert.Equal(t, "apply_server_side_encryption_by_default", parent.Type())
		assert.NotNil(t, attr)
		assert.NotEmpty(t, attr.Value().AsString())
	}
}

func Test_IfConfigFsIsNotSet_ThenUseModuleFsForVars(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"main.tf": `
variable "bucket_name" {
	type = string
}
resource "aws_s3_bucket" "main" {
	bucket = var.bucket_name
}
`,
		"main.tfvars": `bucket_name = "test_bucket"`,
	})
	parser := New(fs, "",
		OptionStopOnHCLError(true),
		OptionWithTFVarsPaths("main.tfvars"),
	)

	require.NoError(t, parser.ParseFS(t.Context(), "."))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	assert.Len(t, modules, 1)

	rootModule := modules[0]
	blocks := rootModule.GetResourcesByType("aws_s3_bucket")
	require.Len(t, blocks, 1)

	block := blocks[0]

	assert.Equal(t, "test_bucket", block.GetAttribute("bucket").AsStringValueOrDefault("", block).Value())
}

func Test_ForEachRefToLocals(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"main.tf": `
locals {
  buckets = toset([
    "foo",
    "bar",
  ])
}

resource "aws_s3_bucket" "this" {
	for_each = local.buckets
	bucket   = each.key
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	assert.Len(t, modules, 1)

	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("aws_s3_bucket")
	assert.Len(t, blocks, 2)

	for _, block := range blocks {
		attr := block.GetAttribute("bucket")
		require.NotNil(t, attr)
		assert.Contains(t, []string{"foo", "bar"}, attr.AsStringValueOrDefault("", block).Value())
	}
}

func Test_ForEachRefToVariableWithDefault(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"main.tf": `
variable "buckets" {
	type    = set(string)
	default = ["foo", "bar"]
}

resource "aws_s3_bucket" "this" {
	for_each = var.buckets
	bucket   = each.key
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	assert.Len(t, modules, 1)

	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("aws_s3_bucket")
	assert.Len(t, blocks, 2)

	for _, block := range blocks {
		attr := block.GetAttribute("bucket")
		require.NotNil(t, attr)
		assert.Contains(t, []string{"foo", "bar"}, attr.AsStringValueOrDefault("", block).Value())
	}
}

func Test_ForEachRefToVariableFromFile(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"main.tf": `
variable "policy_rules" {
  type = object({
    secure_tags = optional(map(object({
      session_matcher        = optional(string)
      priority               = number
      enabled                = optional(bool, true)
    })), {})
  })
}

resource "google_network_security_gateway_security_policy_rule" "secure_tag_rules" {
  for_each               = var.policy_rules.secure_tags
  provider               = google-beta
  project                = "test"
  name                   = each.key
  enabled                = each.value.enabled
  priority               = each.value.priority
  session_matcher        = each.value.session_matcher
}
`,
		"main.tfvars": `
policy_rules = {
  secure_tags = {
    secure-tag-1 = {
      session_matcher = "host() != 'google.com'"
      priority        = 1001
    }
  }
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true), OptionWithTFVarsPaths("main.tfvars"))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	assert.Len(t, modules, 1)

	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("google_network_security_gateway_security_policy_rule")
	assert.Len(t, blocks, 1)

	block := blocks[0]

	assert.Equal(t, "secure-tag-1", block.GetAttribute("name").AsStringValueOrDefault("", block).Value())
	assert.True(t, block.GetAttribute("enabled").AsBoolValueOrDefault(false, block).Value())
	assert.Equal(t, "host() != 'google.com'", block.GetAttribute("session_matcher").AsStringValueOrDefault("", block).Value())
	assert.Equal(t, 1001, block.GetAttribute("priority").AsIntValueOrDefault(0, block).Value())
}

func Test_ForEachRefersToMapThatContainsSameStringValues(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"main.tf": `locals {
  buckets = {
    bucket1 = "test1"
    bucket2 = "test1"
  }
}

resource "aws_s3_bucket" "this" {
  for_each = local.buckets
  bucket = each.key
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	assert.Len(t, modules, 1)

	bucketBlocks := modules.GetResourcesByType("aws_s3_bucket")
	assert.Len(t, bucketBlocks, 2)

	var labels []string

	for _, b := range bucketBlocks {
		labels = append(labels, b.Label())
	}

	expectedLabels := []string{
		`aws_s3_bucket.this["bucket1"]`,
		`aws_s3_bucket.this["bucket2"]`,
	}
	assert.Equal(t, expectedLabels, labels)
}

func TestDataSourceWithCountMetaArgument(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"main.tf": `
data "http" "example" {
  count = 2
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	assert.Len(t, modules, 1)

	rootModule := modules[0]

	httpDataSources := rootModule.GetDatasByType("http")
	assert.Len(t, httpDataSources, 2)

	var labels []string
	for _, b := range httpDataSources {
		labels = append(labels, b.Label())
	}

	expectedLabels := []string{
		`http.example[0]`,
		`http.example[1]`,
	}
	assert.Equal(t, expectedLabels, labels)
}

func TestDataSourceWithForEachMetaArgument(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"main.tf": `
locals {
	ports = ["80", "8080"]
}
data "http" "example" {
  for_each = toset(local.ports)
  url = "localhost:${each.key}"
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	assert.Len(t, modules, 1)

	rootModule := modules[0]

	httpDataSources := rootModule.GetDatasByType("http")
	assert.Len(t, httpDataSources, 2)
}

func TestForEach(t *testing.T) {
	tests := []struct {
		name               string
		src                string
		expectedBucketName string
		expectedNameLabel  string
	}{
		{
			name: "arg is set and ref to each.key",
			src: `locals {
	buckets = ["bucket1"]
}

resource "aws_s3_bucket" "this" {
	for_each = toset(local.buckets)
	bucket = each.key
}`,
			expectedBucketName: "bucket1",
			expectedNameLabel:  `this["bucket1"]`,
		},
		{
			name: "arg is set and ref to each.value",
			src: `locals {
	buckets = ["bucket1"]
}

resource "aws_s3_bucket" "this" {
	for_each = toset(local.buckets)
	bucket = each.value
}`,
			expectedBucketName: "bucket1",
			expectedNameLabel:  `this["bucket1"]`,
		},
		{
			name: "arg is map and ref to each.key",
			src: `locals {
	buckets = {
		bucket1key = "bucket1value"
	}
}

resource "aws_s3_bucket" "this" {
	for_each = local.buckets
	bucket = each.key
}`,
			expectedBucketName: "bucket1key",
			expectedNameLabel:  `this["bucket1key"]`,
		},
		{
			name: "arg is map and ref to each.value",
			src: `locals {
	buckets = {
		bucket1key = "bucket1value"
	}
}

resource "aws_s3_bucket" "this" {
	for_each = local.buckets
	bucket = each.value
}`,
			expectedBucketName: "bucket1value",
			expectedNameLabel:  `this["bucket1key"]`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			modules := parse(t, map[string]string{
				"main.tf": tt.src,
			})
			require.Len(t, modules, 1)

			buckets := modules.GetResourcesByType("aws_s3_bucket")
			assert.Len(t, buckets, 1)

			bucket := buckets[0]
			bucketName := bucket.GetAttribute("bucket").Value().AsString()
			assert.Equal(t, tt.expectedBucketName, bucketName)

			assert.Equal(t, tt.expectedNameLabel, bucket.NameLabel())
		})
	}

}

func TestForEachCountExpanded(t *testing.T) {

	tests := []struct {
		name          string
		source        string
		expectedCount int
	}{
		{
			name: "arg is list of strings",
			source: `locals {
  buckets = ["bucket1", "bucket2"]
}

resource "aws_s3_bucket" "this" {
  for_each = local.buckets
  bucket = each.key
}`,
			expectedCount: 2,
		},
		{
			name: "arg is empty list",
			source: `locals {
  buckets = []
}

resource "aws_s3_bucket" "this" {
  for_each = local.buckets
  bucket   = each.value
}`,
			expectedCount: 0,
		},
		{
			name: "arg is empty set",
			source: `locals {
  buckets = toset([])
}

resource "aws_s3_bucket" "this" {
  for_each = local.buckets
  bucket = each.key
}`,
			expectedCount: 0,
		},
		{
			name: "argument set with the same values",
			source: `locals {
  buckets = ["true", "true"]
}

resource "aws_s3_bucket" "this" {
  for_each = toset(local.buckets)
  bucket = each.key
}`,
			expectedCount: 1,
		},
		{
			name: "arg is non-valid set",
			source: `locals {
  buckets = [{
    bucket1key = "bucket1value"
  }]
}

resource "aws_s3_bucket" "this" {
	for_each = toset(local.buckets)
	bucket = each.value
}`,
			expectedCount: 0,
		},
		{
			name: "arg is set of strings",
			source: `locals {
  buckets = ["bucket1", "bucket2"]
}

resource "aws_s3_bucket" "this" {
  for_each = toset(local.buckets)
  bucket = each.key
}`,
			expectedCount: 2,
		},
		{
			name: "arg is map",
			source: `locals {
  buckets = {
    1 = {}
    2 = {}
  }
}

resource "aws_s3_bucket" "this" {
  for_each = local.buckets
  bucket = each.key
}`,
			expectedCount: 2,
		},
		{
			name: "arg is empty map",
			source: `locals {
	buckets = {}
}
resource "aws_s3_bucket" "this" {
	for_each = local.buckets
	bucket   = each.value
}
		`,
			expectedCount: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			modules := parse(t, map[string]string{
				"main.tf": tt.source,
			})
			assert.Len(t, modules, 1)

			bucketBlocks := modules.GetResourcesByType("aws_s3_bucket")
			assert.Len(t, bucketBlocks, tt.expectedCount)
		})
	}
}

func TestForEachRefToResource(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"main.tf": `
	locals {
  vpcs = {
    "test1" = {
      cidr_block = "192.168.0.0/28"
    }
    "test2" = {
      cidr_block = "192.168.1.0/28"
    }
  }
}

resource "aws_vpc" "example" {
  for_each = local.vpcs
  cidr_block = each.value.cidr_block
}

resource "aws_internet_gateway" "example" {
  for_each = aws_vpc.example
  vpc_id = each.key
}
`,
	})
	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	require.Len(t, modules, 1)

	blocks := modules.GetResourcesByType("aws_internet_gateway")
	require.Len(t, blocks, 2)

	var vpcIds []string
	for _, b := range blocks {
		vpcIds = append(vpcIds, b.GetAttribute("vpc_id").Value().AsString())
	}

	expectedVpcIds := []string{"test1", "test2"}
	assert.Equal(t, expectedVpcIds, vpcIds)
}

func TestArnAttributeOfBucketIsCorrect(t *testing.T) {

	t.Run("the bucket doesn't have a name", func(t *testing.T) {
		fs := testutil.CreateFS(map[string]string{
			"main.tf": `resource "aws_s3_bucket" "this" {}`,
		})
		parser := New(fs, "", OptionStopOnHCLError(true))
		require.NoError(t, parser.ParseFS(t.Context(), "."))

		modules, err := parser.EvaluateAll(t.Context())
		require.NoError(t, err)
		require.Len(t, modules, 1)

		blocks := modules.GetResourcesByType("aws_s3_bucket")
		assert.Len(t, blocks, 1)

		bucket := blocks[0]

		values := bucket.Values()
		arnVal := values.GetAttr("arn")
		assert.True(t, arnVal.Type().Equals(cty.String))

		id := values.GetAttr("id").AsString()

		arn := arnVal.AsString()
		assert.Equal(t, "arn:aws:s3:::"+id, arn)
	})

	t.Run("the bucket has a name", func(t *testing.T) {
		fs := testutil.CreateFS(map[string]string{
			"main.tf": `resource "aws_s3_bucket" "this" {
  bucket = "test"
}

resource "aws_iam_role" "this" {
  name = "test_role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Service = "s3.amazonaws.com"
        }
      },
    ]
  })
}

resource "aws_iam_role_policy" "this" {
  name   = "test_policy"
  role   = aws_iam_role.this.id
  policy = data.aws_iam_policy_document.this.json
}

data "aws_iam_policy_document" "this" {
  statement {
    effect = "Allow"
    actions = [
      "s3:GetObject"
    ]
    resources = ["${aws_s3_bucket.this.arn}/*"]
  }
}`,
		})
		parser := New(fs, "", OptionStopOnHCLError(true))
		require.NoError(t, parser.ParseFS(t.Context(), "."))

		modules, err := parser.EvaluateAll(t.Context())
		require.NoError(t, err)
		require.Len(t, modules, 1)

		blocks := modules[0].GetDatasByType("aws_iam_policy_document")
		assert.Len(t, blocks, 1)

		policyDoc := blocks[0]

		statement := policyDoc.GetBlock("statement")
		resources := statement.GetAttribute("resources").AsStringValueSliceOrEmpty()

		assert.Len(t, resources, 1)
		assert.True(t, resources[0].EqualTo("arn:aws:s3:::test/*"))
	})
}

func TestForEachWithObjectsOfDifferentTypes(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"main.tf": `module "backups" {
  bucket_name  = each.key
  client       = each.value.client
  path_writers = each.value.path_writers

  for_each = {
    "bucket1" = {
      client       = "client1"
      path_writers = ["writer1"] // tuple with string
    },
    "bucket2" = {
      client       = "client2"
      path_writers = [] // empty tuple
    }
  }
}
`,
	})
	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	assert.Len(t, modules, 1)
}

func TestCountMetaArgument(t *testing.T) {
	tests := []struct {
		name     string
		src      string
		expected int
	}{
		{
			name: "zero resources",
			src: `resource "test" "this" {
  count = 0
}`,
			expected: 0,
		},
		{
			name: "several resources",
			src: `resource "test" "this" {
  count = 2
}`,
			expected: 2,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fsys := testutil.CreateFS(map[string]string{
				"main.tf": tt.src,
			})
			parser := New(fsys, "", OptionStopOnHCLError(true))
			require.NoError(t, parser.ParseFS(t.Context(), "."))

			modules, err := parser.EvaluateAll(t.Context())
			require.NoError(t, err)
			assert.Len(t, modules, 1)

			resources := modules.GetResourcesByType("test")
			assert.Len(t, resources, tt.expected)
		})
	}
}

func TestCountMetaArgumentInModule(t *testing.T) {
	tests := []struct {
		name                   string
		files                  map[string]string
		expectedCountModules   int
		expectedCountResources int
	}{
		{
			name: "zero modules",
			files: map[string]string{
				"main.tf": `module "this" {
  count = 0
  source = "./modules/test"
}`,
				"modules/test/main.tf": `resource "test" "this" {}`,
			},
			expectedCountModules:   1,
			expectedCountResources: 0,
		},
		{
			name: "several modules",
			files: map[string]string{
				"main.tf": `module "this" {
  count = 2
  source = "./modules/test"
}`,
				"modules/test/main.tf": `resource "test" "this" {}`,
			},
			expectedCountModules:   3,
			expectedCountResources: 2,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fsys := testutil.CreateFS(tt.files)
			parser := New(fsys, "", OptionStopOnHCLError(true))
			require.NoError(t, parser.ParseFS(t.Context(), "."))

			modules, err := parser.EvaluateAll(t.Context())
			require.NoError(t, err)

			assert.Len(t, modules, tt.expectedCountModules)

			resources := modules.GetResourcesByType("test")
			assert.Len(t, resources, tt.expectedCountResources)
		})
	}
}

func TestDynamicBlocks(t *testing.T) {
	tests := []struct {
		name     string
		src      string
		expected []any
	}{
		{
			name: "for-each use tuple of int",
			src: `resource "test_resource" "test" {
  dynamic "foo" {
    for_each = [80, 443]
    content {
      bar = foo.value
    }
  }
}`,
			expected: []any{float64(80), float64(443)},
		},
		{
			name: "for-each use list of int",
			src: `resource "test_resource" "test" {
  dynamic "foo" {
    for_each = tolist([80, 443])
    content {
      bar = foo.value
    }
  }
}`,
			expected: []any{float64(80), float64(443)},
		},
		{
			name: "for-each use set of int",
			src: `resource "test_resource" "test" {
  dynamic "foo" {
    for_each = toset([80, 443])
    content {
      bar = foo.value
    }
  }
}`,
			expected: []any{float64(80), float64(443)},
		},
		{
			name: "for-each use list of bool",
			src: `resource "test_resource" "test" {
  dynamic "foo" {
    for_each = tolist([true])
    content {
      bar = foo.value
    }
  }
}`,
			expected: []any{true},
		},
		{
			name: "empty for-each",
			src: `resource "test_resource" "test" {
  dynamic "foo" {
    for_each = []
    content {}
  }
}`,
			expected: []any{},
		},
		{
			name: "for-each use tuple of objects",
			src: `variable "test_var" {
  type    = list(object({ enabled = bool }))
  default = [{ enabled = true }]
}

resource "test_resource" "test" {
  dynamic "foo" {
    for_each = var.test_var

    content {
      bar = foo.value.enabled
    }
  }
}`,
			expected: []any{true},
		},
		{
			name: "attribute ref to object key",
			src: `variable "some_var" {
  type = map(
    object({
      tag = string
    })
  )
  default = {
    ssh   = { "tag" = "login" }
    http  = { "tag" = "proxy" }
    https = { "tag" = "proxy" }
  }
}

resource "test_resource" "test" {
  dynamic "foo" {
    for_each = { for name, values in var.some_var : name => values }
    content {
      bar = foo.key
    }
  }
}`,
			expected: []any{"ssh", "http", "https"},
		},
		{
			name: "attribute ref to object value",
			src: `variable "some_var" {
  type = map(
    object({
      tag = string
    })
  )
  default = {
    ssh   = { "tag" = "login" }
    http  = { "tag" = "proxy" }
    https = { "tag" = "proxy" }
  }
}

resource "test_resource" "test" {
  dynamic "foo" {
    for_each = { for name, values in var.some_var : name => values }
    content {
      bar = foo.value.tag
    }
  }
}`,
			expected: []any{"login", "proxy", "proxy"},
		},
		{
			name: "attribute ref to map key",
			src: `variable "some_var" {
  type = map
  default = {
    ssh   = { "tag" = "login" }
    http  = { "tag" = "proxy" }
    https = { "tag" = "proxy" }
  }
}

resource "test_resource" "test" {
  dynamic "foo" {
    for_each = var.some_var
    content {
      bar = foo.key
    }
  }
}`,
			expected: []any{"ssh", "http", "https"},
		},
		{
			name: "attribute ref to map value",
			src: `variable "some_var" {
  type = map
  default = {
    ssh   = { "tag" = "login" }
    http  = { "tag" = "proxy" }
    https = { "tag" = "proxy" }
  }
}

resource "test_resource" "test" {
  dynamic "foo" {
    for_each = var.some_var
    content {
      bar = foo.value.tag
    }
  }
}`,
			expected: []any{"login", "proxy", "proxy"},
		},
		{
			name: "dynamic block with iterator",
			src: `resource "test_resource" "test" {
  dynamic "foo" {
    for_each = ["foo", "bar"]
	iterator = some_iterator
    content {
      bar = some_iterator.value
    }
  }
}`,
			expected: []any{"foo", "bar"},
		},
		{
			name: "iterator and parent block with same name",
			src: `resource "test_resource" "test" {
  dynamic "foo" {
    for_each = ["foo", "bar"]
	iterator = foo
    content {
      bar = foo.value
    }
  }
}`,
			expected: []any{"foo", "bar"},
		},
		{
			name: "for-each use null value",
			src: `resource "test_resource" "test" {
  dynamic "foo" {
    for_each = null
    content {
      bar = foo.value
	}
  }
}`,
			expected: []any{},
		},
		{
			name: "no for-each attribute",
			src: `resource "test_resource" "test" {
  dynamic "foo" {
    content {
      bar = foo.value
	}
  }
}`,
			expected: []any{},
		},
		{
			name: "unknown for-each",
			src: `resource "test_resource" "test" {
  dynamic "foo" {
    for_each = lookup(foo, "") ? [] : []
  }
}`,
			expected: []any{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			modules := parse(t, map[string]string{
				"main.tf": tt.src,
			})
			require.Len(t, modules, 1)

			resource := modules.GetResourcesByType("test_resource")
			require.Len(t, resource, 1)
			blocks := resource[0].GetBlocks("foo")

			var vals []any
			for _, attr := range blocks {
				vals = append(vals, attr.GetAttribute("bar").GetRawValue())
			}

			assert.ElementsMatch(t, tt.expected, vals)
		})
	}
}

func TestNestedDynamicBlock(t *testing.T) {
	modules := parse(t, map[string]string{
		"main.tf": `resource "test_resource" "test" {
  dynamic "foo" {
    for_each = ["1", "1"]
    content {
      dynamic "bar" {
        for_each = [true, true]
        content {
          baz = foo.value
          qux = bar.value
        }
      }
    }
  }
}`,
	})
	require.Len(t, modules, 1)

	testResources := modules.GetResourcesByType("test_resource")
	assert.Len(t, testResources, 1)
	blocks := testResources[0].GetBlocks("foo")
	assert.Len(t, blocks, 2)

	var nested []*terraform.Block
	for _, block := range blocks {
		nested = append(nested, block.GetBlocks("bar")...)
		for _, b := range nested {
			assert.Equal(t, "1", b.GetAttribute("baz").GetRawValue())
			assert.Equal(t, true, b.GetAttribute("qux").GetRawValue())
		}
	}
	assert.Len(t, nested, 4)
}

func parse(t *testing.T, files map[string]string, opts ...Option) terraform.Modules {
	fs := testutil.CreateFS(files)
	opts = append(opts, OptionStopOnHCLError(true))
	parser := New(fs, "", opts...)
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	return modules
}

func TestModuleRefersToOutputOfAnotherModule(t *testing.T) {
	files := map[string]string{
		"main.tf": `
module "module2" {
	source = "./modules/foo"
}

module "module1" {
	source = "./modules/bar"
	test_var = module.module2.test_out
}
`,
		"modules/foo/main.tf": `
output "test_out" {
	value = "test_value"
}
`,
		"modules/bar/main.tf": `
variable "test_var" {}

resource "test_resource" "this" {
	dynamic "dynamic_block" {
		for_each = [var.test_var]
		content {
			some_attr = dynamic_block.value
		}
	}
}
`,
	}

	modules := parse(t, files)
	require.Len(t, modules, 3)

	resources := modules.GetResourcesByType("test_resource")
	require.Len(t, resources, 1)

	attr, _ := resources[0].GetNestedAttribute("dynamic_block.some_attr")
	require.NotNil(t, attr)

	assert.Equal(t, "test_value", attr.GetRawValue())
}

func TestPopulateContextWithBlockInstances(t *testing.T) {

	tests := []struct {
		name      string
		blockType string
		files     map[string]string
	}{
		{
			name:      "data blocks with count",
			blockType: "data",
			files: map[string]string{
				"main.tf": `data "d" "foo" {
  count = 1
  value = "Index ${count.index}"
}

data "b" "foo" {
  count = 1
  value = data.d.foo[0].value
}

data "c" "foo" {
  count = 1
  value = data.b.foo[0].value
}`,
			},
		},
		{
			name:      "resource blocks with count",
			blockType: "resource",
			files: map[string]string{
				"main.tf": `resource "d" "foo" {
  count = 1
  value = "Index ${count.index}"
}

resource "b" "foo" {
  count = 1
  value = d.foo[0].value
}

resource "c" "foo" {
  count = 1
  value = b.foo[0].value
}`,
			},
		},
		{
			name:      "module block with count",
			blockType: "data",
			files: map[string]string{
				"main.tf": `module "a" {
  source = "./modules/a"
  count  = 2
  inp    = "Index ${count.index}"
}

data "b" "foo" {
  count = 1
  value = module.a[0].value
}

data "c" "foo" {
  count = 1
  value = data.b.foo[0].value
}`,
				"modules/a/main.tf": `variable "inp" {}
output "value" {
  value = var.inp
}`,
			},
		},
		{
			name:      "data blocks with for_each",
			blockType: "data",
			files: map[string]string{
				"main.tf": `data "d" "foo" {
  for_each = toset([0])
  value = "Index ${each.key}"
}

data "b" "foo" {
  for_each = data.d.foo 
  value = each.value.value
}

data "c" "foo" {
  for_each = data.b.foo 
  value = each.value.value
}`,
			},
		},
		{
			name:      "resource blocks with for_each",
			blockType: "resource",
			files: map[string]string{
				"main.tf": `resource "d" "foo" {
  for_each = toset([0])
  value = "Index ${each.key}"
}

resource "b" "foo" {
  for_each = d.foo 
  value = each.value.value
}

resource "c" "foo" {
  for_each = b.foo 
  value = each.value.value
}`,
			},
		},
		{
			name:      "module block with for_each",
			blockType: "data",
			files: map[string]string{
				"main.tf": `module "a" {
  for_each = toset([0])
  source = "./modules/a"
  inp    = "Index ${each.key}"
}

data "b" "foo" {
  value = module.a["0"].value
}`,
				"modules/a/main.tf": `variable "inp" {}
output "value" {
  value = var.inp
}`,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			modules := parse(t, tt.files)
			require.GreaterOrEqual(t, len(modules), 1)
			for _, b := range modules.GetBlocks().OfType(tt.blockType) {
				attr := b.GetAttribute("value")
				assert.Equal(t, "Index 0", attr.Value().AsString())
			}
		})
	}
}

// TestNestedModulesOptions ensures parser options are carried to the nested
// submodule evaluators.
// The test will include an invalid module that will fail to download
// if it is attempted.
func TestNestedModulesOptions(t *testing.T) {
	// reset the previous default logger
	prevLog := slog.Default()
	defer slog.SetDefault(prevLog)
	var buf bytes.Buffer
	slog.SetDefault(slog.New(log.NewHandler(&buf, nil)))

	// Folder structure
	// ./
	// ├── main.tf
	// └── modules
	//     ├── city
	//     │   └── main.tf
	//     ├── queens
	//     │   └── main.tf
	//     └── brooklyn
	//         └── main.tf
	//
	// Modules referenced
	// main -> city ├─> brooklyn
	//              └─> queens
	files := map[string]string{
		"main.tf": `
module "city" {
	source = "./modules/city"
}

resource "city" "neighborhoods" {
	names = module.city.neighborhoods
}
`,
		"modules/city/main.tf": `
module "brooklyn" {
	source = "./brooklyn"
}

module "queens" {
	source = "./queens"
}

output "neighborhoods" {
	value = [module.brooklyn.name, module.queens.name]
}
`,
		"modules/city/brooklyn/main.tf": `
output "name" {
	value = "Brooklyn"
}
`,
		"modules/city/queens/main.tf": `
output "name" {
	value = "Queens"
}

module "invalid" {
	source         = "https://example.invaliddomain"
}
`,
	}

	// Using the OptionWithDownloads(false) option will prevent the invalid
	// module from being downloaded. If the log exists "failed to download"
	// then the submodule evaluator attempted to download, which was disallowed.
	modules := parse(t, files, OptionWithDownloads(false))
	require.Len(t, modules, 4)

	resources := modules.GetResourcesByType("city")
	require.Len(t, resources, 1)

	for _, res := range resources {
		attr, _ := res.GetNestedAttribute("names")
		require.NotNil(t, attr, res.FullName())
		assert.Equal(t, []string{"Brooklyn", "Queens"}, attr.GetRawValue())
	}

	require.NotContains(t, buf.String(), "failed to download")

	// Verify module parents are set correctly.
	expectedParents := map[string]string{
		".":                     "",
		"modules/city":          ".",
		"modules/city/brooklyn": "modules/city",
		"modules/city/queens":   "modules/city",
	}

	for _, mod := range modules {
		expected, exists := expectedParents[mod.ModulePath()]
		require.Truef(t, exists, "module %s does not exist in assertion", mod.ModulePath())
		if expected == "" {
			require.Nil(t, mod.Parent())
		} else {
			require.Equal(t, expected, mod.Parent().ModulePath(), "parent of module %q", mod.ModulePath())
		}
	}
}

// TestModuleParents sets up a nested module structure and verifies the
// parent-child relationships are correctly set.
func TestModuleParents(t *testing.T) {
	// The setup is a list of continents, some countries, some cities, etc.
	dirfs := os.DirFS("./testdata/nested")
	parser := New(dirfs, "",
		OptionStopOnHCLError(true),
		OptionWithDownloads(false),
	)
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	// modules only have 'parent'. They do not have children, so create
	// a structure that allows traversal from the root to the leafs.
	modChildren := make(map[*terraform.Module][]*terraform.Module)
	// Keep track of every module that exists
	modSet := set.New[*terraform.Module]()
	var root *terraform.Module
	for _, mod := range modules {
		modChildren[mod] = make([]*terraform.Module, 0)
		modSet.Append(mod)

		if mod.Parent() == nil {
			// Only 1 root should exist
			require.Nil(t, root, "root module already set")
			root = mod
		}
		modChildren[mod.Parent()] = append(modChildren[mod.Parent()], mod)
	}

	type node struct {
		prefix     string
		modulePath string
		children   []node
	}

	// expectedTree is the full module tree structure.
	expectedTree := node{
		modulePath: ".",
		children: []node{
			{
				modulePath: "north-america",
				children: []node{
					{
						modulePath: "north-america/united-states",
						children: []node{
							{modulePath: "north-america/united-states/springfield", prefix: "illinois-"},
							{modulePath: "north-america/united-states/springfield", prefix: "idaho-"},
							{modulePath: "north-america/united-states/new-york", children: []node{
								{modulePath: "north-america/united-states/new-york/new-york-city"},
							}},
						},
					},
					{
						modulePath: "north-america/canada",
						children: []node{
							{modulePath: "north-america/canada/springfield", prefix: "ontario-"},
						},
					},
				},
			},
		},
	}

	var assertChild func(t *testing.T, n node, mod *terraform.Module)
	assertChild = func(t *testing.T, n node, mod *terraform.Module) {
		modSet.Remove(mod)
		children := modChildren[mod]

		t.Run(n.modulePath, func(t *testing.T) {
			if !assert.Len(t, children, len(n.children), "modChildren count for %s", n.modulePath) {
				return
			}
			for _, child := range children {
				// Find the child module that we are expecting.
				idx := slices.IndexFunc(n.children, func(node node) bool {
					outputBlocks := child.GetBlocks().OfType("output")
					outIdx := slices.IndexFunc(outputBlocks, func(outputBlock *terraform.Block) bool {
						return outputBlock.Labels()[0] == "name"
					})
					if outIdx == -1 {
						return false
					}

					output := outputBlocks[outIdx]
					outVal := output.GetAttribute("value").Value()
					if !outVal.Type().Equals(cty.String) {
						return false
					}

					modName := filepath.Base(node.modulePath)
					if outVal.AsString() != node.prefix+modName {
						return false
					}

					return node.modulePath == child.ModulePath()
				})
				if !assert.NotEqualf(t, -1, idx, "module prefix=%s path=%s not found in %s", n.prefix, child.ModulePath(), n.modulePath) {
					continue
				}

				assertChild(t, n.children[idx], child)
			}
		})

	}

	assertChild(t, expectedTree, root)
	// If any module was not asserted, the test will fail. This ensures the
	// entire module tree is checked.
	require.Equal(t, 0, modSet.Size(), "all modules asserted")
}

func TestCyclicModules(t *testing.T) {
	files := map[string]string{
		"main.tf": `
module "module2" {
	source = "./modules/foo"
	test_var = passthru.handover.from_1
}

// Demonstrates need for evaluateSteps between submodule evaluations.
resource "passthru" "handover" {
	from_1 = module.module1.test_out
	from_2 = module.module2.test_out
}

module "module1" {
	source = "./modules/bar"
	test_var = passthru.handover.from_2
}
`,
		"modules/foo/main.tf": `
variable "test_var" {}

resource "test_resource" "this" {
	dynamic "dynamic_block" {
		for_each = [var.test_var]
		content {
			some_attr = dynamic_block.value
		}
	}
}

output "test_out" {
	value = "test_value"
}
`,
		"modules/bar/main.tf": `
variable "test_var" {}

resource "test_resource" "this" {
	dynamic "dynamic_block" {
		for_each = [var.test_var]
		content {
			some_attr = dynamic_block.value
		}
	}
}

output "test_out" {
	value = test_resource.this.dynamic_block.some_attr
}
`,
	}

	modules := parse(t, files)
	require.Len(t, modules, 3)

	resources := modules.GetResourcesByType("test_resource")
	require.Len(t, resources, 2)

	for _, res := range resources {
		attr, _ := res.GetNestedAttribute("dynamic_block.some_attr")
		require.NotNil(t, attr, res.FullName())
		assert.Equal(t, "test_value", attr.GetRawValue())
	}
}

func TestExtractSetValue(t *testing.T) {
	files := map[string]string{
		"main.tf": `
resource "test" "set-value" {
	value = toset(["x", "y", "x"])
}
`,
	}

	resources := parse(t, files).GetResourcesByType("test")
	require.Len(t, resources, 1)
	attr := resources[0].GetAttribute("value")
	require.NotNil(t, attr)
	assert.Equal(t, []string{"x", "y"}, attr.GetRawValue())
}

func TestFunc_fileset(t *testing.T) {
	files := map[string]string{
		"main.tf": `
resource "test" "fileset-func" {
	value = fileset(path.module, "**/*.py")
}
`,
		"a.py":      ``,
		"path/b.py": ``,
	}

	fsys := mapfs.New()
	for name, content := range files {
		fsys.MkdirAll(filepath.Dir(name), 0o755)
		fsys.WriteVirtualFile(name, []byte(content), 0o644)
	}

	parser := New(fsys, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	resources := modules.GetResourcesByType("test")
	require.Len(t, resources, 1)
	attr := resources[0].GetAttribute("value")
	require.NotNil(t, attr)
	assert.Equal(t, []string{"a.py", "path/b.py"}, attr.GetRawValue())
}

func TestExprWithMissingVar(t *testing.T) {
	files := map[string]string{
		"main.tf": `
variable "v" {
	type = string
}

resource "test" "values" {
	s = "foo-${var.v}"
    l1 = ["foo", var.v]
    l2 = concat(["foo"], [var.v])
    d1 = {foo = var.v}
    d2 = merge({"foo": "bar"}, {"baz": var.v})
}
`,
	}

	resources := parse(t, files).GetResourcesByType("test")
	require.Len(t, resources, 1)

	s_attr := resources[0].GetAttribute("s")
	require.NotNil(t, s_attr)
	assert.Equal(t, "foo-", s_attr.Value().Range().StringPrefix())

	for _, name := range []string{"l1", "l2", "d1", "d2"} {
		attr := resources[0].GetAttribute(name)
		require.NotNil(t, attr)
	}
}

func TestVarTypeShortcut(t *testing.T) {
	files := map[string]string{
		"main.tf": `
variable "magic_list" {
	type    = list
	default = ["x", "y"]
}

variable "magic_map" {
	type    = map
	default = {a = 1, b = 2}
}

resource "test" "values" {
	l = var.magic_list
	m = var.magic_map
}
`,
	}

	resources := parse(t, files).GetResourcesByType("test")
	require.Len(t, resources, 1)

	list_attr := resources[0].GetAttribute("l")
	require.NotNil(t, list_attr)
	assert.Equal(t, []string{"x", "y"}, list_attr.GetRawValue())

	map_attr := resources[0].GetAttribute("m")
	require.NotNil(t, map_attr)
	assert.True(t, map_attr.Value().RawEquals(cty.MapVal(map[string]cty.Value{
		"a": cty.NumberIntVal(1), "b": cty.NumberIntVal(2),
	})))
}

func Test_LoadLocalCachedModule(t *testing.T) {
	fsys := os.DirFS(filepath.Join("testdata", "cached-modules"))

	parser := New(
		fsys, "",
		OptionStopOnHCLError(true),
		OptionWithDownloads(false),
	)
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	assert.Len(t, modules, 2)

	buckets := modules.GetResourcesByType("aws_s3_bucket")
	assert.Len(t, buckets, 1)

	assert.Equal(t, "my-private-module/s3-bucket/aws/.terraform/modules/s3-bucket/main.tf", buckets[0].GetMetadata().Range().GetFilename())

	bucketName := buckets[0].GetAttribute("bucket").Value().AsString()
	assert.Equal(t, "my-s3-bucket", bucketName)
}

func TestTFVarsFileDoesNotExist(t *testing.T) {
	fsys := fstest.MapFS{
		"main.tf": &fstest.MapFile{
			Data: []byte(``),
		},
	}

	parser := New(
		fsys, "",
		OptionStopOnHCLError(true),
		OptionWithDownloads(false),
		OptionWithTFVarsPaths("main.tfvars"),
	)
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	_, err := parser.EvaluateAll(t.Context())
	assert.ErrorContains(t, err, "file does not exist")
}

func Test_OptionsWithTfVars(t *testing.T) {
	fs := testutil.CreateFS(map[string]string{
		"main.tf": `resource "test" "this" {
  foo = var.foo
}
variable "foo" {}
`})

	parser := New(fs, "", OptionsWithTfVars(
		map[string]cty.Value{
			"foo": cty.StringVal("bar"),
		},
	))

	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	assert.Len(t, modules, 1)

	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("test")
	assert.Len(t, blocks, 1)
	assert.Equal(t, "bar", blocks[0].GetAttribute("foo").Value().AsString())
}

func Test_AWSRegionNameDefined(t *testing.T) {

	fs := testutil.CreateFS(map[string]string{
		"code/test.tf": `
data "aws_region" "current" {}

data "aws_region" "other" {
  name = "us-east-2"
}

resource "something" "blah" {
  r1 = data.aws_region.current.name
  r2 = data.aws_region.other.name
}
`,
	})

	parser := New(fs, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "code"))
	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	require.Len(t, modules, 1)
	rootModule := modules[0]

	blocks := rootModule.GetResourcesByType("something")
	require.Len(t, blocks, 1)
	block := blocks[0]

	r1 := block.GetAttribute("r1")
	require.NotNil(t, r1)
	assert.True(t, r1.IsResolvable())
	assert.Equal(t, "current-region", r1.Value().AsString())

	r2 := block.GetAttribute("r2")
	require.NotNil(t, r2)
	assert.True(t, r2.IsResolvable())
	assert.Equal(t, "us-east-2", r2.Value().AsString())
}

func TestLogAboutMissingVariableValues(t *testing.T) {
	var buf bytes.Buffer
	slog.SetDefault(slog.New(log.NewHandler(&buf, nil)))

	fsys := fstest.MapFS{
		"main.tf": &fstest.MapFile{
			Data: []byte(`
variable "foo" {}

variable "bar" {
  default = "bar"
}

variable "baz" {}
`),
		},
		"main.tfvars": &fstest.MapFile{
			Data: []byte(`baz = "baz"`),
		},
	}

	parser := New(
		fsys, "",
		OptionStopOnHCLError(true),
		OptionWithTFVarsPaths("main.tfvars"),
	)
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	_, err := parser.Load(t.Context())
	require.NoError(t, err)

	assert.Contains(t, buf.String(), "Variable values were not found in the environment or variable files.")
	assert.Contains(t, buf.String(), "variables=\"foo\"")
}

func TestLoadChildModulesFromLocalCache(t *testing.T) {
	fsys := fstest.MapFS{
		"main.tf": &fstest.MapFile{Data: []byte(`module "level_1" {
  source = "./modules/level_1"
}`)},
		"modules/level_1/main.tf": &fstest.MapFile{Data: []byte(`module "level_2" {
  source  = "../level_2"
}`)},
		"modules/level_2/main.tf": &fstest.MapFile{Data: []byte(`module "level_3" {
  count = 2
  source  = "../level_3"
}`)},
		"modules/level_3/main.tf": &fstest.MapFile{Data: []byte(`resource "foo" "bar" {}`)},
		".terraform/modules/modules.json": &fstest.MapFile{Data: []byte(`{
    "Modules": [
        { "Key": "", "Source": "", "Dir": "." },
        {
            "Key": "level_1",
            "Source": "./modules/level_1",
            "Dir": "modules/level_1"
        },
        {
            "Key": "level_1.level_2",
            "Source": "../level_2",
            "Dir": "modules/level_2"
        },
        {
            "Key": "level_1.level_2.level_3",
            "Source": "../level_3",
            "Dir": "modules/level_3"
        }
    ]
}`)},
	}

	var buf bytes.Buffer
	logger := slog.New(log.NewHandler(&buf, &log.Options{Level: log.LevelDebug}))

	parser := New(
		fsys, "",
		OptionStopOnHCLError(true),
		OptionWithLogger(logger),
	)
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	assert.Len(t, modules, 5)

	assert.Contains(t, buf.String(), "Using module from Terraform cache .terraform/modules\tmodule=\"root\" source=\"./modules/level_1\"")
	assert.Contains(t, buf.String(), "Using module from Terraform cache .terraform/modules\tmodule=\"level_1\" source=\"../level_2\"")
	assert.Contains(t, buf.String(), "Using module from Terraform cache .terraform/modules\tmodule=\"level_2\" source=\"../level_3\"")
	assert.Contains(t, buf.String(), "Using module from Terraform cache .terraform/modules\tmodule=\"level_2\" source=\"../level_3\"")
}

func TestNilParser(t *testing.T) {
	parser := New(
		nil, "",
	)
	err := parser.ParseFS(t.Context(), ".")
	require.Error(t, err)
}

func TestLogParseErrors(t *testing.T) {
	var buf bytes.Buffer
	slog.SetDefault(slog.New(log.NewHandler(&buf, nil)))

	src := `resource "aws-s3-bucket" "name" {
  bucket = <
}`

	fsys := fstest.MapFS{
		"main.tf": &fstest.MapFile{
			Data: []byte(src),
		},
	}

	parser := New(fsys, "")
	err := parser.ParseFS(t.Context(), ".")
	require.NoError(t, err)

	assert.Contains(t, buf.String(), `cause="  bucket = <"`)
}

func Test_PassingNullToChildModule_DoesNotEraseType(t *testing.T) {
	tests := []struct {
		name string
		fsys fs.FS
	}{
		{
			name: "typed variable",
			fsys: fstest.MapFS{
				"main.tf": &fstest.MapFile{Data: []byte(`module "test" {
  source   = "./modules/test"
  test_var = null
}`)},
				"modules/test/main.tf": &fstest.MapFile{Data: []byte(`variable "test_var" {
  type    = number
}

resource "foo" "this" {
  bar = var.test_var != null ? 1 : 2
}`)},
			},
		},
		{
			name: "typed variable with default",
			fsys: fstest.MapFS{
				"main.tf": &fstest.MapFile{Data: []byte(`module "test" {
  source   = "./modules/test"
  test_var = null
}`)},
				"modules/test/main.tf": &fstest.MapFile{Data: []byte(`variable "test_var" {
  type    = number
  default = null
}

resource "foo" "this" {
  bar = var.test_var != null ? 1 : 2
}`)},
			},
		},
		{
			name: "empty variable",
			fsys: fstest.MapFS{
				"main.tf": &fstest.MapFile{Data: []byte(`module "test" {
  source   = "./modules/test"
  test_var = null
}`)},
				"modules/test/main.tf": &fstest.MapFile{Data: []byte(`variable "test_var" {}

resource "foo" "this" {
  bar = var.test_var != null ? 1 : 2
}`)},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := New(
				tt.fsys, "",
				OptionStopOnHCLError(true),
			)
			require.NoError(t, parser.ParseFS(t.Context(), "."))

			_, err := parser.Load(t.Context())
			require.NoError(t, err)

			modules, err := parser.EvaluateAll(t.Context())
			require.NoError(t, err)

			res := modules.GetResourcesByType("foo")[0]
			attr := res.GetAttribute("bar")
			val, _ := attr.Value().AsBigFloat().Int64()
			assert.Equal(t, int64(2), val)
		})
	}
}

func TestAttrRefToNullVariable(t *testing.T) {
	fsys := fstest.MapFS{
		"main.tf": &fstest.MapFile{Data: []byte(`variable "name" {
  type    = string
  default = null
}

resource "aws_s3_bucket" "example" {
  bucket = var.name
}`)},
	}

	parser := New(fsys, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	_, err := parser.Load(t.Context())
	require.NoError(t, err)

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	val := modules.GetResourcesByType("aws_s3_bucket")[0].GetAttribute("bucket").GetRawValue()
	assert.Nil(t, val)
}

func Test_AttrIsRefToOtherBlock(t *testing.T) {
	fsys := testutil.CreateFS(map[string]string{
		"main.tf": `locals {
  baz_idx = 0
}

resource "foo" "bar" {
  attr = baz.qux[local.baz_idx].attr
}

resource "baz" "qux" {
  count = 1
  attr  = "test"
}
`,
	})

	parser := New(fsys, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	require.Len(t, modules, 1)
	root := modules[0]
	foo := root.GetResourcesByType("foo")[0]
	fooAttr := foo.GetAttribute("attr")
	b, err := root.GetReferencedBlock(fooAttr, foo)
	require.NoError(t, err)
	require.NotNil(t, b)
}

func TestConfigWithEphemeralBlock(t *testing.T) {
	fsys := fstest.MapFS{
		"main.tf": &fstest.MapFile{Data: []byte(`ephemeral "random_password" "password" {
  length = 16
}`)},
	}

	parser := New(fsys, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	_, err := parser.Load(t.Context())
	require.NoError(t, err)
}

func TestConvertObjectWithUnknownAndNullValuesToMap(t *testing.T) {
	fsys := fstest.MapFS{
		"main.tf": &fstest.MapFile{Data: []byte(`module "foo" {
  source = "./modules/foo"
}

locals {
  known = "test"
}

module "bar" {
  source = "./modules/bar"
  outputs = {
    "key1" : { "Value" : module.foo.test },
    "key2" : { "Value" : local.known },
    "key3" : { "Value" : local.unknown },
  }
}`)},
		"modules/foo/main.tf": &fstest.MapFile{Data: []byte(`output "test" {
  value       = ref_to_unknown
}`)},
		"modules/bar/main.tf": &fstest.MapFile{Data: []byte(`variable "outputs" {
  type        = map(any)
}`)},
	}

	parser := New(fsys, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	_, err := parser.Load(t.Context())
	require.NoError(t, err)

	_, err = parser.EvaluateAll(t.Context())
	require.NoError(t, err)
}

func TestAttributeWithMissingVarIsUnresolvable(t *testing.T) {
	fsys := fstest.MapFS{
		"main.tf": &fstest.MapFile{Data: []byte(`variable "inp" {
  type = string
}

resource "foo" "bar" {
  attr = "${var.inp}-test"
}
`)},
	}

	parser := New(fsys, "", OptionStopOnHCLError(true))
	require.NoError(t, parser.ParseFS(t.Context(), "."))

	_, err := parser.Load(t.Context())
	require.NoError(t, err)

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)
	require.Len(t, modules, 1)
	foo := modules[0].GetResourcesByType("foo")[0]
	attr := foo.GetAttribute("attr")
	assert.False(t, attr.IsResolvable())
}

// TestInstancedLogger checks if any global logs are generated by the terraform
// parser + evaluation. All logs should be attached to the instanced logger. This
// test does not run all error cases, so it will not capture all log output.
func TestInstancedLogger(t *testing.T) {
	// reset global logger
	prevLog := slog.Default()
	defer slog.SetDefault(prevLog)

	// capture logs to the global logger
	var buf bytes.Buffer

	slog.SetDefault(slog.New(log.NewHandler(&buf, &log.Options{
		Level: slog.LevelDebug,
	})))

	// create a new logger for the parser
	var instance bytes.Buffer
	logger := slog.New(log.NewHandler(&instance, &log.Options{
		Level: slog.LevelDebug,
	}))

	opts := []Option{
		OptionWithLogger(logger),
		OptionStopOnHCLError(false),
		OptionWithDownloads(false),
	}
	// Run some scenarios that will trigger logs
	t.Run("ParserLogs", func(t *testing.T) {
		fsys := fstest.MapFS{
			"main.tf": &fstest.MapFile{
				Data: []byte(`
					bare words
				`)},
		}

		parser := New(fsys, "", opts...)

		// No error is returned, but some parser logs are expected
		err := parser.ParseFS(t.Context(), ".")
		require.NoError(t, err)
		require.NotEmpty(t, instance.String())
		instance.Reset()
	})

	t.Run("Evaluator", func(t *testing.T) {
		fsys := fstest.MapFS{
			"main.tf": &fstest.MapFile{
				Data: []byte(`
					locals {
						phrase = "hello world"
					}

					output "phrase" {
					  value = locals.phrase
					}
				`)},
		}

		parser := New(fsys, "", opts...)

		err := parser.ParseFS(t.Context(), ".")
		require.NoError(t, err)

		_, err = parser.EvaluateAll(t.Context())
		require.NoError(t, err)

		require.NotEmpty(t, instance.String())
		instance.Reset()
	})

	t.Run("ModuleFetching", func(t *testing.T) {
		fsys := testutil.CreateFS(map[string]string{
			"main.tf": `
				module "invalid" {
					source = "totally.invalid"
				}

				module "echo" {
					source = "./echo"				
					input = "hello"
				}

				locals {
					foo = module.echo.value
				}
			`,
			"echo/main.tf": `
				variable "input" {
					type = string				
				}
				output "value" {
					value = var.input 
				}
			`,
		})

		parser := New(fsys, "", opts...)

		err := parser.ParseFS(t.Context(), ".")
		require.NoError(t, err)

		modules, err := parser.EvaluateAll(t.Context())
		require.NoError(t, err)

		require.NotEmpty(t, instance.String())
		require.Len(t, modules, 2)
		instance.Reset()
	})

	//nolint:testifylint // linter wants `emptyf`, but the output is not legible
	if !assert.Emptyf(t, buf.Bytes(), "logs detected in global logger, all logs should be using the instanced logger") {
		t.Log(buf.String()) // Helpful for debugging
	}
}

func TestProvidedWorkingDirectory(t *testing.T) {
	const fakeCwd = "/some/path"
	fsys := testutil.CreateFS(map[string]string{
		"main.tf": `
			resource "foo" "bar" {
			  cwd = path.cwd
			}
		`,
	})

	parser := New(fsys, "", OptionWithWorkingDirectoryPath(fakeCwd))
	err := parser.ParseFS(t.Context(), ".")
	require.NoError(t, err)

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	require.Len(t, modules, 1)
	foo := modules[0].GetResourcesByType("foo")[0]
	attr := foo.GetAttribute("cwd")
	require.Equal(t, fakeCwd, attr.Value().AsString())
}

func Test_ResolveRemoteSubmoduleFromTerraformCache(t *testing.T) {
	fsys := fstest.MapFS{
		"main.tf": &fstest.MapFile{Data: []byte(`module "aws_bucket" {
  source = "github.com/foo/bar.git//aws/modules/bucket?ref=v0.0.1"
}

locals {
  test = module.aws_bucket.out
}
`)},
		".terraform/modules/modules.json": &fstest.MapFile{Data: []byte(`{
    "Modules": [
        { "Key": "", "Source": "", "Dir": "." },
        {
            "Key": "aws_bucket",
            "Source": "git::https://github.com/foo/bar.git//aws/modules/bucket?ref=v0.0.1",
            "Dir": ".terraform/modules/aws_bucket/aws/modules/bucket"
        }
    ]
}`)},
		".terraform/modules/aws_bucket/aws/modules/bucket/main.tf": &fstest.MapFile{Data: []byte(`output "out" {
  value = "some_value"
}`)},
	}

	parser := New(fsys, "",
		OptionWithSkipCachedModules(true),
		OptionWithDownloads(false),
		OptionStopOnHCLError(true),
	)
	err := parser.ParseFS(t.Context(), ".")
	require.NoError(t, err)

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	require.Len(t, modules, 2)
	bucket := modules[0].GetBlocks().OfType("locals")[0]
	attr := bucket.GetAttribute("test")
	require.Equal(t, "some_value", attr.Value().AsString())
}

func Test_UnknownModuleSource(t *testing.T) {
	files := map[string]string{
		`main.tf`: `
module "test" {
  source = "${foo}/modules/foo"
}
`,
	}

	fsys := testutil.CreateFS(files)
	parser := New(fsys, "",
		OptionWithSkipCachedModules(true),
		OptionStopOnHCLError(true),
	)
	err := parser.ParseFS(t.Context(), ".")
	require.NoError(t, err)

	modules, err := parser.EvaluateAll(t.Context())
	require.NoError(t, err)

	require.Len(t, modules, 1)
}

func Test_MarkedValues(t *testing.T) {

	tests := []struct {
		name string
		src  string
	}{
		{
			name: "marked object",
			src: `resource "foo" "bar" {
  test = sensitive({})
}`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			files := map[string]string{
				`main.tf`: tt.src,
			}

			fsys := testutil.CreateFS(files)
			parser := New(fsys, "",
				OptionWithSkipCachedModules(true),
				OptionStopOnHCLError(true),
			)
			err := parser.ParseFS(t.Context(), ".")
			require.NoError(t, err)

			modules, err := parser.EvaluateAll(t.Context())
			require.NoError(t, err)

			require.Len(t, modules, 1)
		})
	}
}
