package cmd

import (
	"bytes"
	"path/filepath"
	"strings"
	"testing"

	"github.com/open-policy-agent/opa/cmd/formats"
	"github.com/open-policy-agent/opa/v1/util/test"
)

func TestParseExit0(t *testing.T) {

	files := map[string]string{
		"x.rego": `package x
		
		p = 1
		`,
	}
	errc, stdout, stderr, _ := testParse(t, files, &configuredParseParams)
	if errc != 0 {
		t.Fatalf("Expected exit code 0, got %v", errc)
	}
	if len(stderr) > 0 {
		t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
	}

	expectedOutput := `module
 package
  ref
   data
   "x"
 rule
  head
   ref
    p
   1
  body
   expr index=0
    true
`

	if got, want := string(stdout), expectedOutput; got != want {
		t.Fatalf("Expected output\n%v\n, got\n%v", want, got)
	}
}

func TestParseExit1(t *testing.T) {

	files := map[string]string{
		"x.rego": `???`,
	}
	errc, _, stderr, _ := testParse(t, files, &configuredParseParams)
	if errc != 1 {
		t.Fatalf("Expected exit code 1, got %v", errc)
	}
	if len(stderr) == 0 {
		t.Fatalf("Expected output in stderr")
	}
}

func TestParseJSONOutput(t *testing.T) {

	files := map[string]string{
		"x.rego": `package x
		
		p = 1
		`,
	}
	errc, stdout, stderr, _ := testParse(t, files, &parseParams{
		format: formats.Flag(formats.JSON, formats.Pretty),
	})
	if errc != 0 {
		t.Fatalf("Expected exit code 0, got %v", errc)
	}
	if len(stderr) > 0 {
		t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
	}

	expectedOutput := `{
  "package": {
    "path": [
      {
        "type": "var",
        "value": "data"
      },
      {
        "type": "string",
        "value": "x"
      }
    ]
  },
  "rules": [
    {
      "body": [
        {
          "index": 0,
          "terms": {
            "type": "boolean",
            "value": true
          }
        }
      ],
      "head": {
        "name": "p",
        "value": {
          "type": "number",
          "value": 1
        },
        "ref": [
          {
            "type": "var",
            "value": "p"
          }
        ]
      }
    }
  ]
}
`

	if got, want := string(stdout), expectedOutput; got != want {
		t.Fatalf("Expected output\n%v\n, got\n%v", want, got)
	}
}

func TestParseJSONOutputWithLocations(t *testing.T) {

	files := map[string]string{
		"x.rego": `package x

p = 1
`,
	}
	errc, stdout, stderr, tempDirPath := testParse(t, files, &parseParams{
		format:      formats.Flag(formats.JSON, formats.Pretty),
		jsonInclude: "locations",
	})
	if errc != 0 {
		t.Fatalf("Expected exit code 0, got %v", errc)
	}
	if len(stderr) > 0 {
		t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
	}

	expectedOutput := strings.ReplaceAll(`{
  "package": {
    "location": {
      "file": "TEMPDIR/x.rego",
      "row": 1,
      "col": 1,
      "text": "cGFja2FnZQ=="
    },
    "path": [
      {
        "location": {
          "file": "TEMPDIR/x.rego",
          "row": 1,
          "col": 9,
          "text": "eA=="
        },
        "type": "var",
        "value": "data"
      },
      {
        "location": {
          "file": "TEMPDIR/x.rego",
          "row": 1,
          "col": 9,
          "text": "eA=="
        },
        "type": "string",
        "value": "x"
      }
    ]
  },
  "rules": [
    {
      "body": [
        {
          "index": 0,
          "location": {
            "file": "TEMPDIR/x.rego",
            "row": 3,
            "col": 5,
            "text": "MQ=="
          },
          "terms": {
            "location": {
              "file": "TEMPDIR/x.rego",
              "row": 3,
              "col": 5,
              "text": "MQ=="
            },
            "type": "boolean",
            "value": true
          }
        }
      ],
      "head": {
        "name": "p",
        "value": {
          "location": {
            "file": "TEMPDIR/x.rego",
            "row": 3,
            "col": 5,
            "text": "MQ=="
          },
          "type": "number",
          "value": 1
        },
        "ref": [
          {
            "location": {
              "file": "TEMPDIR/x.rego",
              "row": 3,
              "col": 1,
              "text": "cA=="
            },
            "type": "var",
            "value": "p"
          }
        ],
        "location": {
          "file": "TEMPDIR/x.rego",
          "row": 3,
          "col": 1,
          "text": "cCA9IDE="
        }
      },
      "location": {
        "file": "TEMPDIR/x.rego",
        "row": 3,
        "col": 1,
        "text": "cCA9IDE="
      }
    }
  ]
}
`, "TEMPDIR", tempDirPath)

	gotLines := strings.Split(string(stdout), "\n")
	wantLines := strings.Split(expectedOutput, "\n")
	min := len(gotLines)
	if len(wantLines) < min {
		min = len(wantLines)
	}

	for i := range min {
		if gotLines[i] != wantLines[i] {
			t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i])
		}
	}

	if len(gotLines) != len(wantLines) {
		t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines))
	}
}

func TestParseRefsJSONOutput(t *testing.T) {

	files := map[string]string{
		"x.rego": `package x
		
    a.b.c := true
		`,
	}
	errc, stdout, stderr, _ := testParse(t, files, &parseParams{
		format: formats.Flag(formats.JSON, formats.Pretty),
	})
	if errc != 0 {
		t.Fatalf("Expected exit code 0, got %v", errc)
	}
	if len(stderr) > 0 {
		t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
	}

	expectedOutput := `{
  "package": {
    "path": [
      {
        "type": "var",
        "value": "data"
      },
      {
        "type": "string",
        "value": "x"
      }
    ]
  },
  "rules": [
    {
      "body": [
        {
          "index": 0,
          "terms": {
            "type": "boolean",
            "value": true
          }
        }
      ],
      "head": {
        "value": {
          "type": "boolean",
          "value": true
        },
        "assign": true,
        "ref": [
          {
            "type": "var",
            "value": "a"
          },
          {
            "type": "string",
            "value": "b"
          },
          {
            "type": "string",
            "value": "c"
          }
        ]
      }
    }
  ]
}
`

	if got, want := string(stdout), expectedOutput; got != want {
		t.Fatalf("Expected output\n%v\n, got\n%v", want, got)
	}
}

func TestParseRefsJSONOutputWithLocations(t *testing.T) {

	files := map[string]string{
		"x.rego": `package x

a.b.c := true
`,
	}
	errc, stdout, stderr, tempDirPath := testParse(t, files, &parseParams{
		format:      formats.Flag(formats.JSON, formats.Pretty),
		jsonInclude: "locations",
	})
	if errc != 0 {
		t.Fatalf("Expected exit code 0, got %v", errc)
	}
	if len(stderr) > 0 {
		t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
	}

	expectedOutput := strings.ReplaceAll(`{
  "package": {
    "location": {
      "file": "TEMPDIR/x.rego",
      "row": 1,
      "col": 1,
      "text": "cGFja2FnZQ=="
    },
    "path": [
      {
        "location": {
          "file": "TEMPDIR/x.rego",
          "row": 1,
          "col": 9,
          "text": "eA=="
        },
        "type": "var",
        "value": "data"
      },
      {
        "location": {
          "file": "TEMPDIR/x.rego",
          "row": 1,
          "col": 9,
          "text": "eA=="
        },
        "type": "string",
        "value": "x"
      }
    ]
  },
  "rules": [
    {
      "body": [
        {
          "index": 0,
          "location": {
            "file": "TEMPDIR/x.rego",
            "row": 3,
            "col": 10,
            "text": "dHJ1ZQ=="
          },
          "terms": {
            "location": {
              "file": "TEMPDIR/x.rego",
              "row": 3,
              "col": 10,
              "text": "dHJ1ZQ=="
            },
            "type": "boolean",
            "value": true
          }
        }
      ],
      "head": {
        "value": {
          "location": {
            "file": "TEMPDIR/x.rego",
            "row": 3,
            "col": 10,
            "text": "dHJ1ZQ=="
          },
          "type": "boolean",
          "value": true
        },
        "assign": true,
        "ref": [
          {
            "location": {
              "file": "TEMPDIR/x.rego",
              "row": 3,
              "col": 1,
              "text": "YQ=="
            },
            "type": "var",
            "value": "a"
          },
          {
            "location": {
              "file": "TEMPDIR/x.rego",
              "row": 3,
              "col": 3,
              "text": "Yg=="
            },
            "type": "string",
            "value": "b"
          },
          {
            "location": {
              "file": "TEMPDIR/x.rego",
              "row": 3,
              "col": 5,
              "text": "Yw=="
            },
            "type": "string",
            "value": "c"
          }
        ],
        "location": {
          "file": "TEMPDIR/x.rego",
          "row": 3,
          "col": 1,
          "text": "YS5iLmMgOj0gdHJ1ZQ=="
        }
      },
      "location": {
        "file": "TEMPDIR/x.rego",
        "row": 3,
        "col": 1,
        "text": "YS5iLmMgOj0gdHJ1ZQ=="
      }
    }
  ]
}
`, "TEMPDIR", tempDirPath)

	gotLines := strings.Split(string(stdout), "\n")
	wantLines := strings.Split(expectedOutput, "\n")
	min := len(gotLines)
	if len(wantLines) < min {
		min = len(wantLines)
	}

	for i := range min {
		if gotLines[i] != wantLines[i] {
			t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i])
		}
	}

	if len(gotLines) != len(wantLines) {
		t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines))
	}
}
func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) {

	files := map[string]string{
		"x.rego": `package x
import rego.v1

default allow = false
allow = true if {
  input.method == "GET"
  input.path = ["getUser", user]
  input.user == user
}
`,
	}
	errc, stdout, stderr, tempDirPath := testParse(t, files, &parseParams{
		format:      formats.Flag(formats.JSON, formats.Pretty),
		jsonInclude: "locations",
	})
	if errc != 0 {
		t.Fatalf("Expected exit code 0, got %v", errc)
	}
	if len(stderr) > 0 {
		t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
	}

	expectedOutput := strings.ReplaceAll(`{
  "package": {
    "location": {
      "file": "TEMPDIR/x.rego",
      "row": 1,
      "col": 1,
      "text": "cGFja2FnZQ=="
    },
    "path": [
      {
        "location": {
          "file": "TEMPDIR/x.rego",
          "row": 1,
          "col": 9,
          "text": "eA=="
        },
        "type": "var",
        "value": "data"
      },
      {
        "location": {
          "file": "TEMPDIR/x.rego",
          "row": 1,
          "col": 9,
          "text": "eA=="
        },
        "type": "string",
        "value": "x"
      }
    ]
  },
  "imports": [
    {
      "location": {
        "file": "TEMPDIR/x.rego",
        "row": 2,
        "col": 1,
        "text": "aW1wb3J0"
      },
      "path": {
        "location": {
          "file": "TEMPDIR/x.rego",
          "row": 2,
          "col": 8,
          "text": "cmVnby52MQ=="
        },
        "type": "ref",
        "value": [
          {
            "location": {
              "file": "TEMPDIR/x.rego",
              "row": 2,
              "col": 8,
              "text": "cmVnbw=="
            },
            "type": "var",
            "value": "rego"
          },
          {
            "location": {
              "file": "TEMPDIR/x.rego",
              "row": 2,
              "col": 13,
              "text": "djE="
            },
            "type": "string",
            "value": "v1"
          }
        ]
      }
    }
  ],
  "rules": [
    {
      "body": [
        {
          "index": 0,
          "location": {
            "file": "TEMPDIR/x.rego",
            "row": 4,
            "col": 1,
            "text": "ZGVmYXVsdA=="
          },
          "terms": {
            "location": {
              "file": "TEMPDIR/x.rego",
              "row": 4,
              "col": 1,
              "text": "ZGVmYXVsdA=="
            },
            "type": "boolean",
            "value": true
          }
        }
      ],
      "default": true,
      "head": {
        "name": "allow",
        "value": {
          "location": {
            "file": "TEMPDIR/x.rego",
            "row": 4,
            "col": 17,
            "text": "ZmFsc2U="
          },
          "type": "boolean",
          "value": false
        },
        "ref": [
          {
            "location": {
              "file": "TEMPDIR/x.rego",
              "row": 4,
              "col": 9,
              "text": "YWxsb3c="
            },
            "type": "var",
            "value": "allow"
          }
        ],
        "location": {
          "file": "TEMPDIR/x.rego",
          "row": 4,
          "col": 9,
          "text": "YWxsb3cgPSBmYWxzZQ=="
        }
      },
      "location": {
        "file": "TEMPDIR/x.rego",
        "row": 4,
        "col": 1,
        "text": "ZGVmYXVsdA=="
      }
    },
    {
      "body": [
        {
          "index": 0,
          "location": {
            "file": "TEMPDIR/x.rego",
            "row": 6,
            "col": 3,
            "text": "aW5wdXQubWV0aG9kID09ICJHRVQi"
          },
          "terms": [
            {
              "location": {
                "file": "TEMPDIR/x.rego",
                "row": 6,
                "col": 16,
                "text": "PT0="
              },
              "type": "ref",
              "value": [
                {
                  "location": {
                    "file": "TEMPDIR/x.rego",
                    "row": 6,
                    "col": 16,
                    "text": "PT0="
                  },
                  "type": "var",
                  "value": "equal"
                }
              ]
            },
            {
              "location": {
                "file": "TEMPDIR/x.rego",
                "row": 6,
                "col": 3,
                "text": "aW5wdXQubWV0aG9k"
              },
              "type": "ref",
              "value": [
                {
                  "location": {
                    "file": "TEMPDIR/x.rego",
                    "row": 6,
                    "col": 3,
                    "text": "aW5wdXQ="
                  },
                  "type": "var",
                  "value": "input"
                },
                {
                  "location": {
                    "file": "TEMPDIR/x.rego",
                    "row": 6,
                    "col": 9,
                    "text": "bWV0aG9k"
                  },
                  "type": "string",
                  "value": "method"
                }
              ]
            },
            {
              "location": {
                "file": "TEMPDIR/x.rego",
                "row": 6,
                "col": 19,
                "text": "IkdFVCI="
              },
              "type": "string",
              "value": "GET"
            }
          ]
        },
        {
          "index": 1,
          "location": {
            "file": "TEMPDIR/x.rego",
            "row": 7,
            "col": 3,
            "text": "aW5wdXQucGF0aCA9IFsiZ2V0VXNlciIsIHVzZXJd"
          },
          "terms": [
            {
              "location": {
                "file": "TEMPDIR/x.rego",
                "row": 7,
                "col": 14,
                "text": "PQ=="
              },
              "type": "ref",
              "value": [
                {
                  "location": {
                    "file": "TEMPDIR/x.rego",
                    "row": 7,
                    "col": 14,
                    "text": "PQ=="
                  },
                  "type": "var",
                  "value": "eq"
                }
              ]
            },
            {
              "location": {
                "file": "TEMPDIR/x.rego",
                "row": 7,
                "col": 3,
                "text": "aW5wdXQucGF0aA=="
              },
              "type": "ref",
              "value": [
                {
                  "location": {
                    "file": "TEMPDIR/x.rego",
                    "row": 7,
                    "col": 3,
                    "text": "aW5wdXQ="
                  },
                  "type": "var",
                  "value": "input"
                },
                {
                  "location": {
                    "file": "TEMPDIR/x.rego",
                    "row": 7,
                    "col": 9,
                    "text": "cGF0aA=="
                  },
                  "type": "string",
                  "value": "path"
                }
              ]
            },
            {
              "location": {
                "file": "TEMPDIR/x.rego",
                "row": 7,
                "col": 16,
                "text": "WyJnZXRVc2VyIiwgdXNlcl0="
              },
              "type": "array",
              "value": [
                {
                  "location": {
                    "file": "TEMPDIR/x.rego",
                    "row": 7,
                    "col": 17,
                    "text": "ImdldFVzZXIi"
                  },
                  "type": "string",
                  "value": "getUser"
                },
                {
                  "location": {
                    "file": "TEMPDIR/x.rego",
                    "row": 7,
                    "col": 28,
                    "text": "dXNlcg=="
                  },
                  "type": "var",
                  "value": "user"
                }
              ]
            }
          ]
        },
        {
          "index": 2,
          "location": {
            "file": "TEMPDIR/x.rego",
            "row": 8,
            "col": 3,
            "text": "aW5wdXQudXNlciA9PSB1c2Vy"
          },
          "terms": [
            {
              "location": {
                "file": "TEMPDIR/x.rego",
                "row": 8,
                "col": 14,
                "text": "PT0="
              },
              "type": "ref",
              "value": [
                {
                  "location": {
                    "file": "TEMPDIR/x.rego",
                    "row": 8,
                    "col": 14,
                    "text": "PT0="
                  },
                  "type": "var",
                  "value": "equal"
                }
              ]
            },
            {
              "location": {
                "file": "TEMPDIR/x.rego",
                "row": 8,
                "col": 3,
                "text": "aW5wdXQudXNlcg=="
              },
              "type": "ref",
              "value": [
                {
                  "location": {
                    "file": "TEMPDIR/x.rego",
                    "row": 8,
                    "col": 3,
                    "text": "aW5wdXQ="
                  },
                  "type": "var",
                  "value": "input"
                },
                {
                  "location": {
                    "file": "TEMPDIR/x.rego",
                    "row": 8,
                    "col": 9,
                    "text": "dXNlcg=="
                  },
                  "type": "string",
                  "value": "user"
                }
              ]
            },
            {
              "location": {
                "file": "TEMPDIR/x.rego",
                "row": 8,
                "col": 17,
                "text": "dXNlcg=="
              },
              "type": "var",
              "value": "user"
            }
          ]
        }
      ],
      "head": {
        "name": "allow",
        "value": {
          "location": {
            "file": "TEMPDIR/x.rego",
            "row": 5,
            "col": 9,
            "text": "dHJ1ZQ=="
          },
          "type": "boolean",
          "value": true
        },
        "ref": [
          {
            "location": {
              "file": "TEMPDIR/x.rego",
              "row": 5,
              "col": 1,
              "text": "YWxsb3c="
            },
            "type": "var",
            "value": "allow"
          }
        ],
        "location": {
          "file": "TEMPDIR/x.rego",
          "row": 5,
          "col": 1,
          "text": "YWxsb3cgPSB0cnVl"
        }
      },
      "location": {
        "file": "TEMPDIR/x.rego",
        "row": 5,
        "col": 1,
        "text": "YWxsb3cgPSB0cnVlIGlmIHsKICBpbnB1dC5tZXRob2QgPT0gIkdFVCIKICBpbnB1dC5wYXRoID0gWyJnZXRVc2VyIiwgdXNlcl0KICBpbnB1dC51c2VyID09IHVzZXIKfQ=="
      }
    }
  ]
}
`, "TEMPDIR", tempDirPath)

	gotLines := strings.Split(string(stdout), "\n")
	wantLines := strings.Split(expectedOutput, "\n")
	min := len(gotLines)
	if len(wantLines) < min {
		min = len(wantLines)
	}

	for i := range min {
		if gotLines[i] != wantLines[i] {
			t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i])
		}
	}

	if len(gotLines) != len(wantLines) {
		t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines))
	}
}

func TestParseJSONOutputComments(t *testing.T) {

	files := map[string]string{
		"x.rego": `package x
		
		# comment
		p = 1
		`,
	}
	errc, stdout, stderr, _ := testParse(t, files, &parseParams{
		format:      formats.Flag(formats.JSON, formats.Pretty),
		jsonInclude: "comments",
	})
	if errc != 0 {
		t.Fatalf("Expected exit code 0, got %v", errc)
	}
	if len(stderr) > 0 {
		t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
	}

	expectedCommentTextValue := "IGNvbW1lbnQ="

	if !strings.Contains(string(stdout), expectedCommentTextValue) {
		t.Fatalf("Comment text value %q missing in output: %s", expectedCommentTextValue, string(stdout))
	}
}

func TestParse_DefaultRegoVersion(t *testing.T) {
	tests := []struct {
		note    string
		module  string
		expErrs []string
	}{
		{
			note: "v0 module",
			module: `package test
a[x] {
	x := 42
}`,
			expErrs: []string{
				"`if` keyword is required before rule body",
				"`contains` keyword is required for partial set rules",
			},
		},
		{
			note: "v1 module",
			module: `package test
a contains x if {
	x := 42
}`,
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			files := map[string]string{
				"test.rego": tc.module,
			}

			_, _, stderr, _ := testParse(t, files, &parseParams{
				format: formats.Flag(formats.Pretty, formats.JSON),
			})

			if len(tc.expErrs) > 0 {
				errs := string(stderr)
				for _, expErr := range tc.expErrs {
					if !strings.Contains(errs, expErr) {
						t.Fatalf("Expected error:\n\n%q\n\ngot:\n\n%s", expErr, errs)
					}
				}
			} else if len(stderr) > 0 {
				t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
			}
		})
	}
}

func TestParseCompatibleFlags(t *testing.T) {
	tests := []struct {
		note         string
		v0Compatible bool
		v1Compatible bool
		policy       string
		expErrs      []string
	}{
		{
			note:         "v0, keywords not used",
			v0Compatible: true,
			policy: `package test
p[v] { 
	v := input.x
}`,
		},
		{
			note:         "v0, keywords not imported",
			v0Compatible: true,
			policy: `package test
p contains v if { 
	v := input.x
}`,
			expErrs: []string{
				"var cannot be used for rule name",
			},
		},
		{
			note:         "v0, keywords imported",
			v0Compatible: true,
			policy: `package test
import future.keywords
p contains v if { 
	v := input.x
}`,
		},
		{
			note:         "v0, rego.v1 imported",
			v0Compatible: true,
			policy: `package test
import rego.v1
p contains v if { 
	v := input.x
}`,
		},

		{
			note:         "v1, keywords not used",
			v1Compatible: true,
			policy: `package test
p[v] { 
	v := input.x
}`,
			expErrs: []string{
				"`if` keyword is required before rule body",
				"`contains` keyword is required for partial set rules",
			},
		},
		{
			note:         "v1, keywords not imported",
			v1Compatible: true,
			policy: `package test
p contains v if { 
	v := input.x
}`,
		},
		{
			note:         "v1, keywords imported",
			v1Compatible: true,
			policy: `package test
import future.keywords
p contains v if { 
	v := input.x
}`,
		},
		{
			note:         "v1, rego.v1 imported",
			v1Compatible: true,
			policy: `package test
import rego.v1
p contains v if { 
	v := input.x
}`,
		},

		// v0 takes precedence over v1
		{
			note:         "v0+v1, keywords not used",
			v0Compatible: true,
			v1Compatible: true,
			policy: `package test
p[v] { 
	v := input.x
}`,
		},
		{
			note:         "v0+v1, keywords not imported",
			v0Compatible: true,
			v1Compatible: true,
			policy: `package test
p contains v if { 
	v := input.x
}`,
			expErrs: []string{
				"var cannot be used for rule name",
			},
		},
		{
			note:         "v0+1, keywords imported",
			v0Compatible: true,
			v1Compatible: true,
			policy: `package test
import future.keywords
p contains v if { 
	v := input.x
}`,
		},
		{
			note:         "v0+v1, rego.v1 imported",
			v0Compatible: true,
			v1Compatible: true,
			policy: `package test
import rego.v1
p contains v if { 
	v := input.x
}`,
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			files := map[string]string{
				"policy.rego": tc.policy,
			}

			_, _, stderr, _ := testParse(t, files, &parseParams{
				format:       formats.Flag(formats.Pretty, formats.JSON),
				v0Compatible: tc.v0Compatible,
				v1Compatible: tc.v1Compatible,
			})

			if len(tc.expErrs) > 0 {
				errs := string(stderr)
				for _, expErr := range tc.expErrs {
					if !strings.Contains(errs, expErr) {
						t.Fatalf("Expected error:\n\n%q\n\ngot:\n\n%s", expErr, errs)
					}
				}
			} else if len(stderr) > 0 {
				t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
			}
		})
	}
}

// Runs parse and returns the exit code, stdout, and stderr contents
func testParse(t *testing.T, files map[string]string, params *parseParams) (int, []byte, []byte, string) {
	t.Helper()

	stdout := new(bytes.Buffer)
	stderr := new(bytes.Buffer)
	var errc int

	var tempDirUsed string
	test.WithTempFS(files, func(path string) {
		var args []string
		for file := range files {
			args = append(args, filepath.Join(path, file))
		}
		errc = parse(args, params, stdout, stderr)

		tempDirUsed = path
	})

	return errc, stdout.Bytes(), stderr.Bytes(), tempDirUsed
}
