// Copyright 2025 The Dawn & Tint Authors.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//  1. Redistributions of source code must retain the above copyright notice, this
//     list of conditions and the following disclaimer.
//
//  2. Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//
//  3. Neither the name of the copyright holder nor the names of its
//     contributors may be used to endorse or promote products derived from
//     this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package build

import (
	"fmt"
	"testing"

	"dawn.googlesource.com/dawn/tools/src/cmd/gen/common"

	"dawn.googlesource.com/dawn/tools/src/cnf"
	"dawn.googlesource.com/dawn/tools/src/container"
	"dawn.googlesource.com/dawn/tools/src/oswrapper"
	"github.com/stretchr/testify/require"
)

func TestLoadExternals(t *testing.T) {
	tests := []struct {
		name            string
		jsonContent     string
		skipWritingFile bool
		want            *Project
		wantErr         bool
		wantErrMsg      string
	}{
		{ /////////////////////////////////////////////////////////////////////////
			name:            "Non-existent file",
			skipWritingFile: true,
			want: &Project{
				externalsJsonPath: "/foo.json",
				externals:         container.NewMap[ExternalDependencyName, ExternalDependency](),
			},
			wantErr:    true,
			wantErrMsg: "open /foo.json: file does not exist",
		},
		{ /////////////////////////////////////////////////////////////////////////
			name:        "Invalid JSON",
			jsonContent: "{\"a\": \"b\"}",
			want: &Project{
				externalsJsonPath: "/foo.json",
				externals:         container.NewMap[ExternalDependencyName, ExternalDependency](),
			},
			wantErr: true,
			wantErrMsg: ("failed to parse '/foo.json': json: cannot unmarshal string into Go value of " +
				"type struct { IncludePatterns []string; Condition string }"),
		},
		// Invalid regex not tested due to not being able to find any patterns that
		// actually cause regexp.Compile() to fail.
		{ /////////////////////////////////////////////////////////////////////////
			name: "Invalid condition",
			jsonContent: `{
  "name": {
    "IncludePatterns": [
      "*"
    ],
    "Condition": "||"
  }
}`,
			want: &Project{
				externalsJsonPath: "/foo.json",
			},
			wantErr: true,
			wantErrMsg: ("/foo.json: could not parse condition: Parse " +
				"error:\n\n||\n^^\n\nexpected 'ident', got '||"),
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Single external no pattern no condition",
			jsonContent: `{
  "name": {
    "IncludePatterns": []
  }
}`,
			want: &Project{
				externalsJsonPath: "/foo.json",
				externals: map[ExternalDependencyName]ExternalDependency{
					"name": ExternalDependency{
						Name:      "name",
						Condition: nil,
					},
				},
			},
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Single external single pattern no condition",
			jsonContent: `{
  "name": {
    "IncludePatterns": [
      "gtest/**"
    ]
  }
}`,
			want: &Project{
				externalsJsonPath: "/foo.json",
				externals: map[ExternalDependencyName]ExternalDependency{
					"name": ExternalDependency{
						Name:      "name",
						Condition: nil,
					},
				},
			},
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Single external multiple patterns no condition",
			jsonContent: `{
  "name": {
    "IncludePatterns": [
      "gtest/**",
      "gmock/**"
    ]
  }
}`,
			want: &Project{
				externalsJsonPath: "/foo.json",
				externals: map[ExternalDependencyName]ExternalDependency{
					"name": ExternalDependency{
						Name:      "name",
						Condition: nil,
					},
				},
			},
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Single external single pattern with condition",
			jsonContent: `{
  "name": {
    "IncludePatterns": [
      "gtest/**"
    ],
    "Condition": "tint_build_spv_reader || tint_build_spv_writer"
  }
}`,
			want: &Project{
				externalsJsonPath: "/foo.json",
				externals: map[ExternalDependencyName]ExternalDependency{
					"name": ExternalDependency{
						Name: "name",
						Condition: cnf.Expr{
							{
								cnf.Unary{
									Negate: false,
									Var:    "tint_build_spv_reader",
								},
								cnf.Unary{
									Negate: false,
									Var:    "tint_build_spv_writer",
								},
							},
						},
					},
				},
			},
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Multiple externals no pattern no condition",
			jsonContent: `{
  "name": {
    "IncludePatterns": []
  },
  "other_name": {
    "IncludePatterns": []
  }
}`,
			want: &Project{
				externalsJsonPath: "/foo.json",
				externals: map[ExternalDependencyName]ExternalDependency{
					"name": ExternalDependency{
						Name:      "name",
						Condition: nil,
					},
					"other_name": ExternalDependency{
						Name:      "other_name",
						Condition: nil,
					},
				},
			},
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Multiple externals single patterns no condition",
			jsonContent: `{
  "name": {
    "IncludePatterns": [
      "gtest/**"
    ]
  },
  "other_name": {
    "IncludePatterns": [
      "gmock/**"
    ]
  }
}`,
			want: &Project{
				externalsJsonPath: "/foo.json",
				externals: map[ExternalDependencyName]ExternalDependency{
					"name": ExternalDependency{
						Name:      "name",
						Condition: nil,
					},
					"other_name": ExternalDependency{
						Name:      "other_name",
						Condition: nil,
					},
				},
			},
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Multiple externals multiple patterns no condition",
			jsonContent: `{
  "name": {
    "IncludePatterns": [
      "gtest/**",
      "foo/**"
    ]
  },
  "other_name": {
    "IncludePatterns": [
      "gmock/**",
      "bar/**"
    ]
  }
}`,
			want: &Project{
				externalsJsonPath: "/foo.json",
				externals: map[ExternalDependencyName]ExternalDependency{
					"name": ExternalDependency{
						Name:      "name",
						Condition: nil,
					},
					"other_name": ExternalDependency{
						Name:      "other_name",
						Condition: nil,
					},
				},
			},
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Multiple externals single patterns with conditions",
			jsonContent: `{
  "name": {
    "IncludePatterns": [
      "gtest/**"
    ],
    "Condition": "tint_build_spv_reader || tint_build_spv_writer"
  },
  "other_name": {
    "IncludePatterns": [
      "gmock/**"
    ],
    "Condition": "!tint_build_spv_reader || !tint_build_spv_writer"
  }
}`,
			want: &Project{
				externalsJsonPath: "/foo.json",
				externals: map[ExternalDependencyName]ExternalDependency{
					"name": ExternalDependency{
						Name: "name",
						Condition: cnf.Expr{
							{
								cnf.Unary{
									Negate: false,
									Var:    "tint_build_spv_reader",
								},
								cnf.Unary{
									Negate: false,
									Var:    "tint_build_spv_writer",
								},
							},
						},
					},
					"other_name": ExternalDependency{
						Name: "other_name",
						Condition: cnf.Expr{
							{
								cnf.Unary{
									Negate: true,
									Var:    "tint_build_spv_reader",
								},
								cnf.Unary{
									Negate: true,
									Var:    "tint_build_spv_writer",
								},
							},
						},
					},
				},
			},
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			jsonFilePath := "/foo.json"
			wrapper := oswrapper.CreateFSTestOSWrapper()
			if !testCase.skipWritingFile {
				err := wrapper.WriteFile(jsonFilePath, []byte(testCase.jsonContent), 0o700)
				require.NoErrorf(t, err, "Error writing file: %v", err)
			}

			p := Project{
				externalsJsonPath: jsonFilePath,
				externals:         container.NewMap[ExternalDependencyName, ExternalDependency](),
			}
			err := loadExternals(&p, wrapper)
			if testCase.wantErr {
				require.ErrorContains(t, err, testCase.wantErrMsg)
			} else {
				require.NoErrorf(t, err, "Error loading externals: %v", err)
			}
			// We can't compare the entire structs to each other since
			// includePatternMatch will be an anonymous function.
			require.Equal(t, testCase.want.externalsJsonPath, p.externalsJsonPath)
			require.Len(t, p.externals, len(testCase.want.externals))
			for k := range p.externals {
				require.Contains(t, testCase.want.externals, k)
				require.Equal(t, testCase.want.externals[k].Name, p.externals[k].Name)
				require.Equal(t, testCase.want.externals[k].Condition, p.externals[k].Condition)
			}
		})
	}
}

func TestPopulateSourceFiles(t *testing.T) {
	tests := []struct {
		name                         string
		skipFileCreation             bool
		wantDirectories              []string
		wantTargets                  [][]string
		wantFiles                    [][]string
		wantGeneratedProtobufSources [][]string
		wantErr                      bool
		wantErrMsg                   string
	}{
		{ /////////////////////////////////////////////////////////////////////////
			name:             "Non-existent root",
			skipFileCreation: true,
			wantErr:          true,
			wantErrMsg:       "open root: file does not exist",
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Success",
			wantDirectories: []string{
				"a",
				"b",
			},
			wantTargets: [][]string{
				{
					"a",
					string(targetLib),
				},
				{
					"a",
					string(targetProto),
				},
				{
					"a",
					string(targetTestCmd),
				},
				{
					"b",
					string(targetLib),
				},
				{
					"b",
					string(targetProto),
				},
				{
					"b",
					string(targetTestCmd),
				},
			},
			wantFiles: [][]string{
				{
					"a",
					string(targetLib),
					"file.cc",
				},
				{
					"a",
					string(targetLib),
					"file.h",
				},
				{
					"a",
					string(targetLib),
					"file.mm",
				},
				{
					"a",
					string(targetProto),
					"file.proto",
				},
				{
					"a",
					string(targetTestCmd),
					"main_test.cc",
				},
				{
					"b",
					string(targetLib),
					"file.cc",
				},
				{
					"b",
					string(targetLib),
					"file.h",
				},
				{
					"b",
					string(targetLib),
					"file.mm",
				},
				{
					"b",
					string(targetProto),
					"file.proto",
				},
				{
					"b",
					string(targetTestCmd),
					"main_test.cc",
				},
			},
			wantGeneratedProtobufSources: [][]string{
				{
					"a",
					string(targetProto),
					"file.pb.cc",
				},
				{
					"a",
					string(targetProto),
					"file.pb.h",
				},
				{
					"b",
					string(targetProto),
					"file.pb.cc",
				},
				{
					"b",
					string(targetProto),
					"file.pb.h",
				},
			},
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			wrapper := oswrapper.CreateFSTestOSWrapper()
			if !testCase.skipFileCreation {
				parentDirs := []string{"root", "not_root"}
				childDirs := []string{"a", "b", "fuzzers"}
				fileNames := []string{
					"file.cc", "file.h", "file.inl", "file.mm", "file.proto", "file.txt", "main_test.cc"}
				for _, pd := range parentDirs {
					for _, cd := range childDirs {
						wrapper.MkdirAll(fmt.Sprintf("/%s/%s", pd, cd), 0o700)
						for _, fn := range fileNames {
							wrapper.Create(fmt.Sprintf("/%s/%s/%s", pd, cd, fn))
						}
					}
				}
			}

			p := Project{
				Root:        "/root",
				Files:       container.NewMap[string, *File](),
				Directories: container.NewMap[string, *Directory](),
				Targets:     container.NewMap[TargetName, *Target](),
			}

			// Project structs become way too verbose to reasonably list out
			// explicitly in the test cases, so generate them here.
			want := Project{
				Root:        "/root",
				Files:       container.NewMap[string, *File](),
				Directories: container.NewMap[string, *Directory](),
				Targets:     container.NewMap[TargetName, *Target](),
			}
			for _, d := range testCase.wantDirectories {
				want.AddDirectory(d)
			}
			for _, t := range testCase.wantTargets {
				directory := want.Directories[t[0]]
				kind := TargetKind(t[1])
				want.AddTarget(directory, kind)
			}
			for _, f := range testCase.wantFiles {
				directoryName := f[0]
				kind := TargetKind(f[1])
				targetName := directoryName
				if !kind.IsLib() {
					targetName = fmt.Sprintf("%s:%s", directoryName, kind)
				}
				filepath := fmt.Sprintf("%s/%s", directoryName, f[2])
				target := want.Targets[TargetName(targetName)]
				target.AddSourceFile(want.AddFile(filepath))
			}
			for _, f := range testCase.wantGeneratedProtobufSources {
				directoryName := f[0]
				kind := TargetKind(f[1])
				targetName := directoryName
				if !kind.IsLib() {
					targetName = fmt.Sprintf("%s:%s", directoryName, kind)
				}
				filepath := fmt.Sprintf("%s/%s", directoryName, f[2])
				target := want.Targets[TargetName(targetName)]
				target.AddGeneratedProtobufSource(want.AddGeneratedProtobufSource(filepath))
			}

			err := populateSourceFiles(&p, wrapper)
			if testCase.wantErr {
				require.ErrorContains(t, err, testCase.wantErrMsg)
			} else {
				require.NoErrorf(t, err, "Error populating source files: %v", err)
			}
			require.Equal(t, want, p)
		})
	}
}

func TestPopulateSourceFiles_TemplateDetection(t *testing.T) {
	wrapper := oswrapper.CreateFSTestOSWrapper()
	cfg := &common.Config{OsWrapper: wrapper}
	p := NewProject("/root", cfg)

	wrapper.MkdirAll("/root/a", 0o700)
	wrapper.Create("/root/a/file.cc")
	wrapper.Create("/root/a/file2.cc.tmpl")

	err := populateSourceFiles(p, wrapper)
	require.NoError(t, err)

	dir := p.AddDirectory("a")
	target := p.Target(dir, targetLib)
	require.NotNil(t, target, "Target not created")

	require.True(t, target.SourceFileSet.Contains("a/file.cc"))
	require.True(t, target.GeneratedSourcePaths.Contains("a/file2.cc"))
	require.True(t, p.AllTemplatePaths.Contains("a/file2.cc.tmpl"))
	require.False(t, target.SourceFileSet.Contains("a/file2.cc.tmpl"))
	require.True(t, target.TemplateFileSet.Contains("a/file2.cc.tmpl"))
}

func TestBuildDependencies_TemplateIncludes(t *testing.T) {
	wrapper := oswrapper.CreateFSTestOSWrapper()
	cfg := &common.Config{OsWrapper: wrapper}
	p := NewProject("/root", cfg)

	wrapper.MkdirAll("/root/a", 0o700)
	wrapper.MkdirAll("/root/b", 0o700)

	wrapper.Create("/root/a/file.cc")
	wrapper.WriteFile("/root/a/file2.cc.tmpl", []byte(`#include "src/tint/b/file.h"`), 0o600)
	wrapper.Create("/root/b/file.h")

	err := populateSourceFiles(p, wrapper)
	require.NoError(t, err)

	err = scanSourceFiles(p, wrapper)
	require.NoError(t, err)

	err = buildDependencies(p, wrapper)
	require.NoError(t, err)

	dirA := p.AddDirectory("a")
	dirB := p.AddDirectory("b")
	targetA := p.Target(dirA, targetLib)
	targetB := p.Target(dirB, targetLib)

	require.NotNil(t, targetA, "Target A not created")
	require.NotNil(t, targetB, "Target B not created")

	found := false
	for _, dep := range targetA.Dependencies.Internal() {
		if dep == targetB {
			found = true
			break
		}
	}
	require.True(t, found, "Target A should depend on Target B")
}

func TestBuildDependencies_TemplateFallback(t *testing.T) {
	wrapper := oswrapper.CreateFSTestOSWrapper()
	cfg := &common.Config{OsWrapper: wrapper}
	p := NewProject("/root", cfg)

	wrapper.MkdirAll("/root/a", 0o700)
	wrapper.MkdirAll("/root/b", 0o700)

	wrapper.WriteFile("/root/a/file.cc", []byte(`#include "src/tint/b/file.h"`), 0o600)
	wrapper.Create("/root/b/file.h.tmpl")

	err := populateSourceFiles(p, wrapper)
	require.NoError(t, err)

	err = scanSourceFiles(p, wrapper)
	require.NoError(t, err)

	err = buildDependencies(p, wrapper)
	require.NoError(t, err)

	dirA := p.AddDirectory("a")
	dirB := p.AddDirectory("b")
	targetA := p.Target(dirA, targetLib)
	targetB := p.Target(dirB, targetLib)

	require.NotNil(t, targetA, "Target A not created")
	require.NotNil(t, targetB, "Target B not created")

	found := false
	for _, dep := range targetA.Dependencies.Internal() {
		if dep == targetB {
			found = true
			break
		}
	}
	require.True(t, found, "Target A should depend on Target B (via template fallback)")
}

func TestCheckInclude(t *testing.T) {
	tests := []struct {
		name             string
		file             *File
		include          Include
		includeCondition Condition
		wantErr          bool
		wantErrMsg       string
	}{
		{ /////////////////////////////////////////////////////////////////////////
			name: "Success no conditions",
			file: &File{
				Name:      "file.cc",
				Condition: Condition{},
				Directory: &Directory{
					Path: "/root",
				},
				Target: &Target{
					Condition: Condition{},
				},
			},
			include:          Include{},
			includeCondition: Condition{},
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Success all conditions met",
			file: &File{
				Name: "file.cc",
				Condition: Condition{
					{
						cnf.Unary{
							Negate: false,
							Var:    "foo",
						},
					},
				},
				Directory: &Directory{
					Path: "/root",
				},
				Target: &Target{
					Condition: Condition{
						{
							cnf.Unary{
								Negate: true,
								Var:    "bar",
							},
						},
					},
				},
			},
			include: Include{
				Condition: Condition{
					{
						cnf.Unary{
							Negate: false,
							Var:    "baz",
						},
					},
				},
			},
			includeCondition: Condition{
				{
					cnf.Unary{
						Negate: false,
						Var:    "foo",
					},
				},
				{
					cnf.Unary{
						Negate: true,
						Var:    "bar",
					},
				},
				{
					cnf.Unary{
						Negate: false,
						Var:    "baz",
					},
				},
			},
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Success all conditions met underspecified",
			file: &File{
				Name: "file.cc",
				Condition: Condition{
					{
						cnf.Unary{
							Negate: false,
							Var:    "foo",
						},
					},
				},
				Directory: &Directory{
					Path: "/root",
				},
				Target: &Target{
					Condition: Condition{
						{
							cnf.Unary{
								Negate: true,
								Var:    "bar",
							},
						},
					},
				},
			},
			include: Include{
				Condition: Condition{
					{
						cnf.Unary{
							Negate: false,
							Var:    "baz",
						},
					},
				},
			},
			includeCondition: Condition{
				{
					cnf.Unary{
						Negate: false,
						Var:    "foo",
					},
				},
			},
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "includeCondition not met no source conditions",
			file: &File{
				Name:      "file.cc",
				Condition: Condition{},
				Directory: &Directory{
					Path: "/root",
				},
				Target: &Target{
					Condition: Condition{},
				},
			},
			include: Include{},
			includeCondition: Condition{
				{
					cnf.Unary{
						Negate: false,
						Var:    "foo",
					},
				},
			},
			wantErr: true,
			wantErrMsg: `/root/file.cc:0 #include "" requires guard: #if FOO

/root/file.cc build conditions: <none>
 build conditions: foo`,
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "File condition not met",
			file: &File{
				Name: "file.cc",
				Condition: Condition{
					{
						cnf.Unary{
							Negate: true,
							Var:    "foo",
						},
					},
				},
				Directory: &Directory{
					Path: "/root",
				},
				Target: &Target{
					Condition: Condition{
						{
							cnf.Unary{
								Negate: true,
								Var:    "bar",
							},
						},
					},
				},
			},
			include: Include{
				Condition: Condition{
					{
						cnf.Unary{
							Negate: false,
							Var:    "baz",
						},
					},
				},
			},
			includeCondition: Condition{
				{
					cnf.Unary{
						Negate: false,
						Var:    "foo",
					},
				},
				{
					cnf.Unary{
						Negate: true,
						Var:    "bar",
					},
				},
				{
					cnf.Unary{
						Negate: false,
						Var:    "baz",
					},
				},
			},
			wantErr: true,
			wantErrMsg: `/root/file.cc:0 #include "" requires guard: #if FOO

/root/file.cc build conditions: baz && (!foo) && (!bar)
 build conditions: foo && (!bar) && baz`,
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Target condition not met",
			file: &File{
				Name: "file.cc",
				Condition: Condition{
					{
						cnf.Unary{
							Negate: false,
							Var:    "foo",
						},
					},
				},
				Directory: &Directory{
					Path: "/root",
				},
				Target: &Target{
					Condition: Condition{
						{
							cnf.Unary{
								Negate: false,
								Var:    "bar",
							},
						},
					},
				},
			},
			include: Include{
				Condition: Condition{
					{
						cnf.Unary{
							Negate: false,
							Var:    "baz",
						},
					},
				},
			},
			includeCondition: Condition{
				{
					cnf.Unary{
						Negate: false,
						Var:    "foo",
					},
				},
				{
					cnf.Unary{
						Negate: true,
						Var:    "bar",
					},
				},
				{
					cnf.Unary{
						Negate: false,
						Var:    "baz",
					},
				},
			},
			wantErr: true,
			wantErrMsg: `/root/file.cc:0 #include "" requires guard: #if (!BAR)

/root/file.cc build conditions: baz && foo && bar
 build conditions: foo && (!bar) && baz`,
		},
		{ /////////////////////////////////////////////////////////////////////////
			name: "Include condition not met",
			file: &File{
				Name: "file.cc",
				Condition: Condition{
					{
						cnf.Unary{
							Negate: false,
							Var:    "foo",
						},
					},
				},
				Directory: &Directory{
					Path: "/root",
				},
				Target: &Target{
					Condition: Condition{
						{
							cnf.Unary{
								Negate: true,
								Var:    "bar",
							},
						},
					},
				},
			},
			include: Include{
				Condition: Condition{
					{
						cnf.Unary{
							Negate: true,
							Var:    "baz",
						},
					},
				},
			},
			includeCondition: Condition{
				{
					cnf.Unary{
						Negate: false,
						Var:    "foo",
					},
				},
				{
					cnf.Unary{
						Negate: true,
						Var:    "bar",
					},
				},
				{
					cnf.Unary{
						Negate: false,
						Var:    "baz",
					},
				},
			},
			wantErr: true,
			wantErrMsg: `/root/file.cc:0 #include "" requires guard: #if BAZ

/root/file.cc build conditions: (!baz) && foo && (!bar)
 build conditions: foo && (!bar) && baz`,
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			err := checkInclude(testCase.file, testCase.include, testCase.includeCondition)
			if testCase.wantErr {
				require.ErrorContains(t, err, testCase.wantErrMsg)
			} else {
				require.NoErrorf(t, err, "Got error checking include: %v", err)
			}
		})
	}
}

func TestCheckForCycles_EmptyGraph(t *testing.T) {
	wrapper := oswrapper.CreateFSTestOSWrapper()
	project := &Project{
		Targets: container.NewMap[TargetName, *Target](),
	}

	err := checkForCycles(project, wrapper)
	require.NoErrorf(t, err, "Error checking for cycles: %v", err)
}

func TestCheckForCycles_NoCycle(t *testing.T) {
	wrapper := oswrapper.CreateFSTestOSWrapper()
	project := &Project{}
	project.Targets = map[TargetName]*Target{
		"target_1": &Target{
			Name: "target_1",
			Dependencies: &Dependencies{
				internal: container.NewSet(TargetName("target_2")),
				project:  project,
			},
		},
		"target_2": &Target{
			Name: "target_2",
			Dependencies: &Dependencies{
				project: project,
			},
		},
	}

	err := checkForCycles(project, wrapper)
	require.NoErrorf(t, err, "Error checking for cycles: %v", err)
}

func TestCheckForCycles_Cycle(t *testing.T) {
	wrapper := oswrapper.CreateFSTestOSWrapper()
	project := &Project{}
	project.Targets = map[TargetName]*Target{
		"target_1": &Target{
			Name: "target_1",
			Dependencies: &Dependencies{
				internal: container.NewSet(TargetName("target_2")),
				project:  project,
			},
		},
		"target_2": &Target{
			Name: "target_2",
			Dependencies: &Dependencies{
				internal: container.NewSet(TargetName("target_1")),
				project:  project,
			},
		},
	}

	err := checkForCycles(project, wrapper)
	require.ErrorContains(t, err, `cyclic target dependency found:
   target_1
   target_2
   target_1
   target_1
`)
}

func TestEmitDotFile_Empty(t *testing.T) {
	wrapper := oswrapper.CreateFSTestOSWrapper()
	wrapper.MkdirAll("/root", 0o700)
	project := &Project{
		Targets: container.NewMap[TargetName, *Target](),
		Root:    "/root",
	}

	err := emitDotFile(project, targetLib, wrapper)
	require.NoErrorf(t, err, "Error emitting dot file: %v", err)

	bytes, err := wrapper.ReadFile(fmt.Sprintf("/root/%v.dot", targetLib))
	require.NoErrorf(t, err, "Error reading dot file: %v", err)
	expectedContents := `strict digraph {
  node [ shape = "box" ]
  node [ fontname = "Courier" ]
  node [ fontsize = "14" ]
  node [ style = "filled,rounded" ]
  node [ fillcolor = "yellow" ]
  edge [ fontname = "Courier" ]
  edge [ fontsize = "12" ]
}
`
	require.Equal(t, expectedContents, string(bytes[:]))
}

func TestEmitDotFile_OnlyMatchingKindIncluded(t *testing.T) {
	wrapper := oswrapper.CreateFSTestOSWrapper()
	wrapper.MkdirAll("/root", 0o700)
	project := &Project{
		Root: "/root",
	}
	project.Targets = map[TargetName]*Target{
		"target_1": &Target{
			Name: "target_1",
			Kind: targetLib,
			Dependencies: &Dependencies{
				internal: container.NewSet(TargetName("target_2"), TargetName("target_3")),
				project:  project,
			},
		},
		"target_2": &Target{
			Name: "target_2",
			Kind: targetLib,
			Dependencies: &Dependencies{
				project: project,
			},
		},
		"target_3": &Target{
			Name: "target_3",
			Kind: targetProto,
			Dependencies: &Dependencies{
				project: project,
			},
		},
	}

	err := emitDotFile(project, targetLib, wrapper)
	require.NoErrorf(t, err, "Error emitting dot file: %v", err)

	bytes, err := wrapper.ReadFile(fmt.Sprintf("/root/%v.dot", targetLib))
	require.NoErrorf(t, err, "Error reading dot file: %v", err)
	expectedContents := `strict digraph {
  node [ shape = "box" ]
  node [ fontname = "Courier" ]
  node [ fontsize = "14" ]
  node [ style = "filled,rounded" ]
  node [ fillcolor = "yellow" ]
  edge [ fontname = "Courier" ]
  edge [ fontsize = "12" ]
  n0 [label="target_1"]
  n1 [label="target_2"]
  n0 -> n1 [label=""]
  n0 -> n0 [label=""]
}
`
	require.Equal(t, expectedContents, string(bytes[:]))
}
