package parser

import (
	"strings"
	"testing"
	"time"

	"github.com/unkn0wn-root/resterm/internal/parser/directive/options"
	"github.com/unkn0wn-root/resterm/internal/restfile"
)

func hasParseMessage(list []restfile.ParseError, sub string) bool {
	for _, e := range list {
		if strings.Contains(e.Message, sub) {
			return true
		}
	}
	return false
}

func TestParseAuthAndSettings(t *testing.T) {
	src := `# @name Sample
# @auth bearer token-123
# @setting timeout 5s
# @tag smoke critical
# @capture global authToken {{response.json.token}}
GET https://example.com/api
> {% tests.assert(true, "status ok") %}
`

	doc := Parse("sample.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}

	req := doc.Requests[0]
	if req.Metadata.Auth == nil {
		t.Fatalf("expected auth metadata")
	}
	if req.Metadata.Auth.Type != "bearer" {
		t.Fatalf("expected bearer auth, got %s", req.Metadata.Auth.Type)
	}
	if req.Metadata.Auth.Params["token"] != "token-123" {
		t.Fatalf("unexpected bearer token: %q", req.Metadata.Auth.Params["token"])
	}

	if req.Settings["timeout"] != "5s" {
		t.Fatalf("expected timeout setting 5s, got %q", req.Settings["timeout"])
	}

	if len(req.Metadata.Tags) != 2 {
		t.Fatalf("expected two tags, got %v", req.Metadata.Tags)
	}

	if len(req.Metadata.Scripts) != 1 {
		t.Fatalf("expected one script block, got %d", len(req.Metadata.Scripts))
	}
	script := req.Metadata.Scripts[0]
	if script.Kind != "test" {
		t.Fatalf("expected test script, got %s", script.Kind)
	}
	if script.Body == "" {
		t.Fatalf("expected script body to be captured")
	}

	if len(req.Metadata.Captures) != 1 {
		t.Fatalf("expected one capture, got %d", len(req.Metadata.Captures))
	}
	capture := req.Metadata.Captures[0]
	if capture.Scope != restfile.CaptureScopeGlobal {
		t.Fatalf("expected global capture scope, got %v", capture.Scope)
	}
	if capture.Name != "authToken" {
		t.Fatalf("unexpected capture name: %s", capture.Name)
	}
	if capture.Expression != "{{response.json.token}}" {
		t.Fatalf("unexpected capture expression: %q", capture.Expression)
	}
}

func TestParseFileScopedAuthDirective(t *testing.T) {
	src := `# @auth file command argv=["gh","auth","token"] cache_key=github
GET https://example.com
`

	doc := Parse("file-auth.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Auth) != 1 {
		t.Fatalf("expected 1 auth profile, got %d", len(doc.Auth))
	}
	prof := doc.Auth[0]
	if prof.Scope != restfile.AuthScopeFile {
		t.Fatalf("expected file scope, got %v", prof.Scope)
	}
	if prof.Spec.Type != "command" {
		t.Fatalf("expected command auth type, got %q", prof.Spec.Type)
	}
	if prof.Spec.Params["cache_key"] != "github" {
		t.Fatalf("expected cache_key github, got %q", prof.Spec.Params["cache_key"])
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	if doc.Requests[0].Metadata.Auth != nil {
		t.Fatalf("expected request auth to remain unset until execution")
	}
}

func TestParseRequestAuthNone(t *testing.T) {
	src := `# @auth none
GET https://example.com
`

	doc := Parse("request-auth-none.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Metadata.Auth != nil {
		t.Fatalf("expected request auth to be cleared")
	}
	if !req.Metadata.AuthDisabled {
		t.Fatalf("expected request auth to be explicitly disabled")
	}
}

func TestParseExplicitRequestScopedAuth(t *testing.T) {
	src := `# @auth request bearer token-123
GET https://example.com
`

	doc := Parse("request-auth-explicit.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Metadata.Auth == nil {
		t.Fatalf("expected request auth metadata")
	}
	if req.Metadata.Auth.Type != "bearer" {
		t.Fatalf("expected bearer auth, got %q", req.Metadata.Auth.Type)
	}
	if req.Metadata.AuthDisabled {
		t.Fatalf("expected auth disable flag to remain false")
	}
}

func TestParseMethodLineWithHTTPVersion(t *testing.T) {
	src := `###

GET http://127.0.0.1:5001/games HTTP/1.1

###

GET http://127.0.0.1:5001/games/1 HTTP/1.1
`

	doc := Parse("version.http", []byte(src))
	if len(doc.Requests) != 2 {
		t.Fatalf("expected 2 requests, got %d", len(doc.Requests))
	}

	if doc.Requests[0].URL != "http://127.0.0.1:5001/games" {
		t.Fatalf("unexpected first url: %q", doc.Requests[0].URL)
	}
	if doc.Requests[1].URL != "http://127.0.0.1:5001/games/1" {
		t.Fatalf("unexpected second url: %q", doc.Requests[1].URL)
	}
	if doc.Requests[0].Settings["http-version"] != "1.1" {
		t.Fatalf("expected http-version=1.1, got %q", doc.Requests[0].Settings["http-version"])
	}
	if doc.Requests[1].Settings["http-version"] != "1.1" {
		t.Fatalf("expected http-version=1.1, got %q", doc.Requests[1].Settings["http-version"])
	}
}

func TestHTTPVersionSettingOverridesRequestLine(t *testing.T) {
	src := `GET https://example.com HTTP/1.1
# @setting http-version 2
`

	doc := Parse("override.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	if doc.Requests[0].Settings["http-version"] != "2" {
		t.Fatalf("expected http-version=2, got %q", doc.Requests[0].Settings["http-version"])
	}
}

func TestParseAssertDirective(t *testing.T) {
	src := `# @assert status == 200
# @assert contains(header("Content-Type"), "json") => "content type"
GET https://example.com/api
`
	doc := Parse("assert.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Asserts) != 2 {
		t.Fatalf("expected 2 asserts, got %d", len(req.Metadata.Asserts))
	}
	first := req.Metadata.Asserts[0]
	if first.Expression != "status == 200" {
		t.Fatalf("unexpected assert expression: %q", first.Expression)
	}
	if first.Line != 1 {
		t.Fatalf("expected line 1, got %d", first.Line)
	}
	second := req.Metadata.Asserts[1]
	if second.Expression != `contains(header("Content-Type"), "json")` {
		t.Fatalf("unexpected assert expression: %q", second.Expression)
	}
	if second.Message != "content type" {
		t.Fatalf("unexpected assert message: %q", second.Message)
	}
	if second.Line != 2 {
		t.Fatalf("expected line 2, got %d", second.Line)
	}
}

func TestSplitAssertEscapes(t *testing.T) {
	expr, msg := splitAssert(`contains(body, "a=>b") => "ok"`)
	if expr != `contains(body, "a=>b")` {
		t.Fatalf("unexpected expr: %q", expr)
	}
	if msg != "ok" {
		t.Fatalf("unexpected msg: %q", msg)
	}

	expr, msg = splitAssert(`contains(body, "a\"=>b") => "ok"`)
	if expr != `contains(body, "a\"=>b")` {
		t.Fatalf("unexpected escaped expr: %q", expr)
	}
	if msg != "ok" {
		t.Fatalf("unexpected escaped msg: %q", msg)
	}
}

func TestParseScriptLang(t *testing.T) {
	src := `# @script pre-request lang=rts
> request.setHeader("X-Test", "1")
GET https://example.com
`
	doc := Parse("script-lang.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Scripts) != 1 {
		t.Fatalf("expected 1 script block, got %d", len(req.Metadata.Scripts))
	}
	script := req.Metadata.Scripts[0]
	if script.Kind != "pre-request" {
		t.Fatalf("expected pre-request script, got %s", script.Kind)
	}
	if script.Lang != "rts" {
		t.Fatalf("expected rts lang, got %q", script.Lang)
	}
	if script.Body == "" {
		t.Fatalf("expected script body to be captured")
	}
}

func TestParseRTSDirectiveAlias(t *testing.T) {
	tests := []struct {
		name string
		dir  string
		kind string
	}{
		{
			name: "pre request",
			dir:  "# @rts pre-request",
			kind: "pre-request",
		},
		{
			name: "language option",
			dir:  "# @rts pre-request lang=rts",
			kind: "pre-request",
		},
		{
			name: "restermlang option",
			dir:  "# @rts pre-request language=restermlang",
			kind: "pre-request",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			src := strings.Join([]string{
				tt.dir,
				`> request.setHeader("X-Test", "1")`,
				"GET https://example.com",
				"",
			}, "\n")

			doc := Parse("rts-alias.http", []byte(src))
			if len(doc.Errors) != 0 {
				t.Fatalf("expected no parse errors, got %v", doc.Errors)
			}
			if len(doc.Requests) != 1 {
				t.Fatalf("expected 1 request, got %d", len(doc.Requests))
			}
			req := doc.Requests[0]
			if len(req.Metadata.Scripts) != 1 {
				t.Fatalf("expected 1 script block, got %d", len(req.Metadata.Scripts))
			}
			script := req.Metadata.Scripts[0]
			if script.Kind != tt.kind {
				t.Fatalf("expected %s script, got %s", tt.kind, script.Kind)
			}
			if script.Lang != "rts" {
				t.Fatalf("expected rts lang, got %q", script.Lang)
			}
			if script.Body == "" {
				t.Fatalf("expected script body to be captured")
			}
		})
	}
}

func TestParseInlineRTSSourceLines(t *testing.T) {
	src := `### Sample
# @rts pre-request
> request.setHeader("X", missing.value)
  > pre.mutate(request, vars, env)
GET https://example.com
`

	doc := Parse("script-source.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Scripts) != 1 {
		t.Fatalf("expected 1 script block, got %d", len(req.Metadata.Scripts))
	}

	script := req.Metadata.Scripts[0]
	if script.SourcePath != "script-source.http" {
		t.Fatalf("unexpected source path: %q", script.SourcePath)
	}
	wantBody := "request.setHeader(\"X\", missing.value)\npre.mutate(request, vars, env)"
	if script.Body != wantBody {
		t.Fatalf("unexpected script body:\n%s", script.Body)
	}
	wantLines := []restfile.ScriptLine{{Line: 3, Col: 3}, {Line: 4, Col: 5}}
	if len(script.Lines) != len(wantLines) {
		t.Fatalf("expected %d script lines, got %#v", len(wantLines), script.Lines)
	}
	for i, want := range wantLines {
		if script.Lines[i] != want {
			t.Fatalf("line %d: expected %#v, got %#v", i, want, script.Lines[i])
		}
	}
}

func TestParseRTSDirectiveRejectsUnsupportedModes(t *testing.T) {
	tests := []struct {
		name string
		dir  string
		want string
	}{
		{
			name: "bare directive",
			dir:  "# @rts",
			want: "@rts requires a mode",
		},
		{
			name: "language token without mode",
			dir:  "# @rts rts",
			want: "@rts requires a mode",
		},
		{
			name: "language option without mode",
			dir:  "# @rts lang=rts",
			want: "@rts requires a mode",
		},
		{
			name: "pre alias",
			dir:  "# @rts pre",
			want: "@rts supports only pre-request mode",
		},
		{
			name: "duplicate kind",
			dir:  "# @rts pre-request pre-request",
			want: "@rts accepts only one mode",
		},
		{
			name: "test kind",
			dir:  "# @rts test",
			want: "@rts test is not supported",
		},
		{
			name: "tests kind",
			dir:  "# @rts tests",
			want: "@rts test is not supported",
		},
		{
			name: "javascript option",
			dir:  "# @rts pre-request lang=js",
			want: "@rts only supports RestermScript",
		},
		{
			name: "javascript token",
			dir:  "# @rts js",
			want: "@rts only supports RestermScript",
		},
		{
			name: "unknown kind",
			dir:  "# @rts response",
			want: "@rts supports only pre-request mode",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			src := strings.Join([]string{
				tt.dir,
				`> request.setHeader("X-Test", "1")`,
				"GET https://example.com",
				"",
			}, "\n")

			doc := Parse("rts-invalid.http", []byte(src))
			if !hasParseMessage(doc.Errors, tt.want) {
				t.Fatalf("expected parse error containing %q, got %v", tt.want, doc.Errors)
			}
			if len(doc.Requests) != 1 {
				t.Fatalf("expected 1 request, got %d", len(doc.Requests))
			}
			if got := len(doc.Requests[0].Metadata.Scripts); got != 0 {
				t.Fatalf("expected invalid @rts body to be discarded, got %d scripts", got)
			}
		})
	}
}

func TestParseRTSDirectiveAliasFileInclude(t *testing.T) {
	src := `# @rts pre-request
> < ./pre.rts
GET https://example.com
`
	doc := Parse("rts-include.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Scripts) != 1 {
		t.Fatalf("expected 1 script block, got %d", len(req.Metadata.Scripts))
	}
	script := req.Metadata.Scripts[0]
	if script.Kind != "pre-request" {
		t.Fatalf("expected pre-request script, got %s", script.Kind)
	}
	if script.Lang != "rts" {
		t.Fatalf("expected rts lang, got %q", script.Lang)
	}
	if script.FilePath != "./pre.rts" {
		t.Fatalf("unexpected script file path: %q", script.FilePath)
	}
	if script.Body != "" {
		t.Fatalf("expected script body to be empty for file include, got %q", script.Body)
	}
}

func TestParseApplyDirective(t *testing.T) {
	src := `# @apply {headers: {"X-Test": "1"}}
GET https://example.com
`
	doc := Parse("apply.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Applies) != 1 {
		t.Fatalf("expected 1 apply directive, got %d", len(req.Metadata.Applies))
	}
	spec := req.Metadata.Applies[0]
	if spec.Expression != `{headers: {"X-Test": "1"}}` {
		t.Fatalf("unexpected apply expression: %q", spec.Expression)
	}
	if len(spec.Uses) != 0 {
		t.Fatalf("expected no apply uses, got %#v", spec.Uses)
	}
	if spec.Line != 1 {
		t.Fatalf("expected line 1, got %d", spec.Line)
	}
	if spec.Col != 1 {
		t.Fatalf("expected col 1, got %d", spec.Col)
	}
}

func TestParseApplyUseChain(t *testing.T) {
	src := `# @apply use=jsonApi,use=authProd,use=strict
GET https://example.com
`
	doc := Parse("apply-use.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	ap := doc.Requests[0].Metadata.Applies
	if len(ap) != 1 {
		t.Fatalf("expected 1 apply directive, got %d", len(ap))
	}
	sp := ap[0]
	if sp.Expression != "" {
		t.Fatalf("expected empty expression for use chain, got %q", sp.Expression)
	}
	want := []string{"jsonApi", "authProd", "strict"}
	if len(sp.Uses) != len(want) {
		t.Fatalf("unexpected apply uses: %#v", sp.Uses)
	}
	for i, v := range want {
		if sp.Uses[i] != v {
			t.Fatalf("expected use %q at index %d, got %q", v, i, sp.Uses[i])
		}
	}
}

func TestParseApplyUseInvalidToken(t *testing.T) {
	src := `# @apply use=ok,bad=oops
GET https://example.com
`
	doc := Parse("apply-use-bad.http", []byte(src))
	if len(doc.Errors) == 0 {
		t.Fatalf("expected parse errors")
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	if got := len(doc.Requests[0].Metadata.Applies); got != 0 {
		t.Fatalf("expected 0 apply directives on error, got %d", got)
	}
}

func TestParsePatchProfiles(t *testing.T) {
	src := `# @patch file jsonApi {headers: {"Accept":"application/json"}}
# @patch global strict {settings: {timeout:"3s"}}
GET https://example.com
`
	doc := Parse("patch.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Patches) != 2 {
		t.Fatalf("expected 2 patch profiles, got %d", len(doc.Patches))
	}
	if doc.Patches[0].Scope != restfile.PatchScopeFile {
		t.Fatalf("expected first patch to be file scope, got %v", doc.Patches[0].Scope)
	}
	if doc.Patches[0].Name != "jsonApi" {
		t.Fatalf("unexpected first patch name %q", doc.Patches[0].Name)
	}
	if doc.Patches[1].Scope != restfile.PatchScopeGlobal {
		t.Fatalf("expected second patch to be global scope, got %v", doc.Patches[1].Scope)
	}
	if doc.Patches[1].Name != "strict" {
		t.Fatalf("unexpected second patch name %q", doc.Patches[1].Name)
	}
}

func TestParsePatchInsideRequestErrors(t *testing.T) {
	src := `### sample
# @name Sample
# @patch file local {headers: {"X-Test":"1"}}
GET https://example.com
`
	doc := Parse("patch-request.http", []byte(src))
	if len(doc.Errors) == 0 {
		t.Fatalf("expected parse errors")
	}
	if len(doc.Patches) != 0 {
		t.Fatalf("expected no patch profiles when declared in request, got %d", len(doc.Patches))
	}
}

func TestFileLevelSettingsCaptured(t *testing.T) {
	src := `# @setting timeout 10s
# @setting http-insecure true

### First
GET https://example.com
`
	doc := Parse("file-settings.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request")
	}
	if doc.Settings == nil {
		t.Fatalf("expected file-level settings to be captured")
	}
	if doc.Settings["timeout"] != "10s" {
		t.Fatalf("expected timeout=10s, got %q", doc.Settings["timeout"])
	}
	if doc.Settings["http-insecure"] != "true" {
		t.Fatalf("expected http-insecure=true, got %q", doc.Settings["http-insecure"])
	}
}

func TestSettingsDirectiveMultiAssignFileScope(t *testing.T) {
	src := `# @settings timeout=10s http-insecure=true
# @settings proxy=http://one

### First
GET https://example.com
`
	doc := Parse("file-settings.http", []byte(src))
	if doc.Settings == nil {
		t.Fatalf("expected settings to be captured")
	}
	if doc.Settings["timeout"] != "10s" {
		t.Fatalf("expected timeout=10s, got %q", doc.Settings["timeout"])
	}
	if doc.Settings["http-insecure"] != "true" {
		t.Fatalf("expected http-insecure=true, got %q", doc.Settings["http-insecure"])
	}
	if doc.Settings["proxy"] != "http://one" {
		t.Fatalf("expected proxy=http://one, got %q", doc.Settings["proxy"])
	}
}

func TestSettingsDirectiveRequestScopeOverride(t *testing.T) {
	src := `# @settings timeout=9s followredirects=true

### First
# @name req1
# @setting proxy http://legacy
# @settings timeout=3s proxy=http://req followredirects=false
GET https://example.com
`
	doc := Parse("request-settings.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Settings["timeout"] != "3s" {
		t.Fatalf("expected request timeout=3s, got %q", req.Settings["timeout"])
	}
	if req.Settings["proxy"] != "http://req" {
		t.Fatalf("expected request proxy override, got %q", req.Settings["proxy"])
	}
	if req.Settings["followredirects"] != "false" {
		t.Fatalf("expected request followredirects=false, got %q", req.Settings["followredirects"])
	}
	if doc.Settings["timeout"] != "9s" {
		t.Fatalf("expected file timeout=9s, got %q", doc.Settings["timeout"])
	}
	if doc.Settings["followredirects"] != "true" {
		t.Fatalf("expected file followredirects=true, got %q", doc.Settings["followredirects"])
	}
}

func TestParseGlobalDirectiveWhitespaceValue(t *testing.T) {
	src := `# @global base_url https://httpbin.org
# @global alt_url: https://alt.example.com
GET https://example.com
`

	doc := Parse("globals.http", []byte(src))

	if len(doc.Globals) != 2 {
		t.Fatalf("expected 2 globals, got %d", len(doc.Globals))
	}

	values := make(map[string]string)
	for _, gv := range doc.Globals {
		values[gv.Name] = gv.Value
	}

	if values["base_url"] != "https://httpbin.org" {
		t.Fatalf("expected base_url to be https://httpbin.org, got %q", values["base_url"])
	}

	if values["alt_url"] != "https://alt.example.com" {
		t.Fatalf("expected alt_url to be https://alt.example.com, got %q", values["alt_url"])
	}
}

func TestParseScopedVariableCommentAliases(t *testing.T) {
	src := `# @file api.base https://example.com
# @file-secret api.token secret-file

### WithRequest
# @request request.id 42
# @request-secret request.token secret-req
GET https://example.com
`

	doc := Parse("scopes-aliases.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}

	fileVars := make(map[string]restfile.Variable)
	for _, v := range doc.Variables {
		fileVars[v.Name] = v
	}
	if v, ok := fileVars["api.base"]; !ok || v.Scope != restfile.ScopeFile || v.Secret {
		t.Fatalf("expected file api.base as non-secret file var, got %#v", v)
	}
	if v, ok := fileVars["api.token"]; !ok || v.Scope != restfile.ScopeFile || !v.Secret {
		t.Fatalf("expected file-secret api.token, got %#v", v)
	}

	req := doc.Requests[0]
	reqVars := make(map[string]restfile.Variable)
	for _, v := range req.Variables {
		reqVars[v.Name] = v
	}
	if v, ok := reqVars["request.id"]; !ok || v.Scope != restfile.ScopeRequest || v.Secret {
		t.Fatalf("expected request id as non-secret request var, got %#v", v)
	}
	if v, ok := reqVars["request.token"]; !ok || v.Scope != restfile.ScopeRequest || !v.Secret {
		t.Fatalf("expected request-secret token, got %#v", v)
	}
}

func TestParseShorthandSecretVariables(t *testing.T) {
	src := `@global-secret auth.token super-secret
@file-secret base.url https://secret.example.com

### InlineRequest
@request-secret trace.id {{$uuid}}
GET https://example.com
`

	doc := Parse("shorthand-secret.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}

	if len(doc.Globals) != 1 || doc.Globals[0].Name != "auth.token" || !doc.Globals[0].Secret {
		t.Fatalf("expected one secret global, got %#v", doc.Globals)
	}

	if len(doc.Variables) != 1 || doc.Variables[0].Name != "base.url" || !doc.Variables[0].Secret ||
		doc.Variables[0].Scope != restfile.ScopeFile {
		t.Fatalf("expected one secret file variable, got %#v", doc.Variables)
	}

	req := doc.Requests[0]
	if len(req.Variables) != 1 {
		t.Fatalf("expected one request variable, got %d", len(req.Variables))
	}
	if rv := req.Variables[0]; rv.Name != "trace.id" || rv.Scope != restfile.ScopeRequest ||
		!rv.Secret {
		t.Fatalf("unexpected request var: %#v", rv)
	}
}

func TestParseConstDirectives(t *testing.T) {
	t.Parallel()

	src := `# @const svc.http http://localhost:8080
# @const greeting Hello World

GET {{svc.http}}/status
`

	doc := Parse("const.http", []byte(src))
	if doc == nil {
		t.Fatalf("expected document")
	}
	if len(doc.Constants) != 2 {
		t.Fatalf("expected 2 constants, got %d", len(doc.Constants))
	}
	consts := make(map[string]restfile.Constant)
	for _, c := range doc.Constants {
		consts[c.Name] = c
	}
	if got := consts["svc.http"].Value; got != "http://localhost:8080" {
		t.Fatalf("expected svc.http to be http://localhost:8080, got %q", got)
	}
	if got := consts["greeting"].Value; got != "Hello World" {
		t.Fatalf("expected greeting to be Hello World, got %q", got)
	}
}

func TestParseSSHGlobalProfile(t *testing.T) {
	src := `# @ssh global edge host=10.0.0.5 user=ops key=~/.ssh/id_ed25519 port=2022 known_hosts=/tmp/kh strict_hostkey=false persist timeout=30s keepalive=15s retries=2
GET http://example.com
`
	doc := Parse("ssh.http", []byte(src))
	if len(doc.SSH) != 1 {
		t.Fatalf("expected 1 ssh profile, got %d", len(doc.SSH))
	}

	prof := doc.SSH[0]
	if prof.Scope != restfile.SSHScopeGlobal {
		t.Fatalf("expected global scope, got %v", prof.Scope)
	}
	if prof.Name != "edge" {
		t.Fatalf("expected name edge, got %q", prof.Name)
	}
	if prof.Host != "10.0.0.5" {
		t.Fatalf("unexpected host %q", prof.Host)
	}
	if prof.Port != 2022 || prof.PortStr != "2022" {
		t.Fatalf("unexpected port %d (%q)", prof.Port, prof.PortStr)
	}
	if prof.User != "ops" {
		t.Fatalf("unexpected user %q", prof.User)
	}
	if prof.Key == "" {
		t.Fatalf("expected key to be set")
	}
	if prof.KnownHosts != "/tmp/kh" {
		t.Fatalf("unexpected known_hosts %q", prof.KnownHosts)
	}
	if !prof.Strict.Set || prof.Strict.Val {
		t.Fatalf("expected strict_hostkey=false to be set")
	}
	if !prof.Persist.Set || !prof.Persist.Val {
		t.Fatalf("expected persist to be set true")
	}
	if !prof.Timeout.Set || prof.TimeoutStr != "30s" {
		t.Fatalf("timeout not captured, val=%v raw=%q", prof.Timeout.Val, prof.TimeoutStr)
	}
	if !prof.KeepAlive.Set || prof.KeepAliveStr != "15s" {
		t.Fatalf("keepalive not captured, val=%v raw=%q", prof.KeepAlive.Val, prof.KeepAliveStr)
	}
	if !prof.Retries.Set || prof.RetriesStr != "2" {
		t.Fatalf("retries not captured")
	}
}

func TestParseSSHGlobalProfileAliases(t *testing.T) {
	src := `# @ssh global edge host=10.0.0.5 user=ops pass=secret known-hosts=/tmp/kh strict-hostkey=false
GET http://example.com
`
	doc := Parse("ssh_aliases.http", []byte(src))
	if len(doc.SSH) != 1 {
		t.Fatalf("expected 1 ssh profile, got %d", len(doc.SSH))
	}

	prof := doc.SSH[0]
	if prof.Pass != "secret" {
		t.Fatalf("unexpected pass %q", prof.Pass)
	}
	if prof.KnownHosts != "/tmp/kh" {
		t.Fatalf("unexpected known-hosts %q", prof.KnownHosts)
	}
	if !prof.Strict.Set || prof.Strict.Val {
		t.Fatalf("expected strict-hostkey=false to be set")
	}
}

func TestParseSSHRequestUseOverride(t *testing.T) {
	src := `### jump
# @ssh use=edge host={{api_host}} strict_hostkey=false retries=3
GET http://{{api_host}}
`
	doc := Parse("ssh_req.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.SSH == nil {
		t.Fatalf("expected ssh spec on request")
	}
	if req.SSH.Use != "edge" {
		t.Fatalf("unexpected use %q", req.SSH.Use)
	}
	if req.SSH.Inline == nil {
		t.Fatalf("expected inline overrides")
	}
	inline := req.SSH.Inline
	if inline.Scope != restfile.SSHScopeRequest {
		t.Fatalf("expected request scope inline, got %v", inline.Scope)
	}
	if inline.Host != "{{api_host}}" {
		t.Fatalf("unexpected inline host %q", inline.Host)
	}
	if !inline.Strict.Set || inline.Strict.Val {
		t.Fatalf("expected strict_hostkey=false")
	}
	if !inline.Retries.Set || inline.RetriesStr != "3" {
		t.Fatalf("expected retries override")
	}
}

func TestParseSSHRequestIgnoresPersist(t *testing.T) {
	src := `### jump
# @ssh request host=1.2.3.4 user=ops persist keepalive=1s
GET http://example.com
`
	doc := Parse("ssh_req_persist.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.SSH == nil || req.SSH.Inline == nil {
		t.Fatalf("expected inline ssh profile")
	}
	if req.SSH.Inline.Persist.Set {
		t.Fatalf("request persist should be ignored")
	}
	if !hasParseMessage(doc.Warnings, "@ssh request scope ignores persist") {
		t.Fatalf("expected warning for ignored ssh persist, got %v", doc.Warnings)
	}
}

func TestParseSSHWithGRPCRequest(t *testing.T) {
	src := `### grpc over ssh
# @ssh use=jump
# @grpc test.Inventory/Seed
GRPC passthrough:///grpc-internal:8082

{}
`
	doc := Parse("grpc_ssh.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.GRPC == nil {
		t.Fatalf("expected grpc request to be parsed")
	}
	if req.SSH == nil {
		t.Fatalf("expected ssh spec on grpc request")
	}
	if req.SSH.Use != "jump" {
		t.Fatalf("unexpected ssh use %q", req.SSH.Use)
	}
}

func TestParseK8sGlobalProfile(t *testing.T) {
	src := `# @k8s global cluster-api namespace=default pod=api-server port=8080 context=kind-dev kubeconfig=~/.kube/config local_port=18080 address=127.0.0.1 pod_running_timeout=30s retries=2 persist
GET http://example.com
`
	doc := Parse("k8s.http", []byte(src))
	if len(doc.K8s) != 1 {
		t.Fatalf("expected 1 k8s profile, got %d", len(doc.K8s))
	}

	prof := doc.K8s[0]
	if prof.Scope != restfile.K8sScopeGlobal {
		t.Fatalf("expected global scope, got %v", prof.Scope)
	}
	if prof.Name != "cluster-api" {
		t.Fatalf("expected profile name cluster-api, got %q", prof.Name)
	}
	if prof.Namespace != "default" {
		t.Fatalf("unexpected namespace %q", prof.Namespace)
	}
	if prof.Target != "pod:api-server" {
		t.Fatalf("unexpected target %q", prof.Target)
	}
	if prof.Pod != "api-server" {
		t.Fatalf("unexpected pod %q", prof.Pod)
	}
	if prof.Port != 8080 || prof.PortStr != "8080" {
		t.Fatalf("unexpected port %d (%q)", prof.Port, prof.PortStr)
	}
	if prof.Context != "kind-dev" {
		t.Fatalf("unexpected context %q", prof.Context)
	}
	if prof.Kubeconfig != "~/.kube/config" {
		t.Fatalf("unexpected kubeconfig %q", prof.Kubeconfig)
	}
	if prof.LocalPort != 18080 || prof.LocalPortStr != "18080" {
		t.Fatalf("unexpected local port %d (%q)", prof.LocalPort, prof.LocalPortStr)
	}
	if prof.Address != "127.0.0.1" {
		t.Fatalf("unexpected address %q", prof.Address)
	}
	if !prof.PodWait.Set || prof.PodWaitStr != "30s" {
		t.Fatalf("expected pod_running_timeout to be captured")
	}
	if !prof.Retries.Set || prof.RetriesStr != "2" {
		t.Fatalf("expected retries to be captured")
	}
	if !prof.Persist.Set || !prof.Persist.Val {
		t.Fatalf("expected persist=true to be captured")
	}
}

func TestParseK8sGlobalProfileAliases(t *testing.T) {
	src := `# @k8s global cluster-api ns=default svc=api port=8080 kube-context=kind-dev config=~/.kube/config localport=18080 bind=127.0.0.1 podwait=30s retries=2 persist
GET http://example.com
`
	doc := Parse("k8s_aliases.http", []byte(src))
	if len(doc.K8s) != 1 {
		t.Fatalf("expected 1 k8s profile, got %d", len(doc.K8s))
	}

	prof := doc.K8s[0]
	if prof.Namespace != "default" {
		t.Fatalf("unexpected namespace %q", prof.Namespace)
	}
	if prof.Target != "service:api" {
		t.Fatalf("unexpected target %q", prof.Target)
	}
	if prof.Context != "kind-dev" {
		t.Fatalf("unexpected context %q", prof.Context)
	}
	if prof.Kubeconfig != "~/.kube/config" {
		t.Fatalf("unexpected kubeconfig %q", prof.Kubeconfig)
	}
	if prof.LocalPort != 18080 || prof.LocalPortStr != "18080" {
		t.Fatalf("unexpected local port %d (%q)", prof.LocalPort, prof.LocalPortStr)
	}
	if prof.Address != "127.0.0.1" {
		t.Fatalf("unexpected address %q", prof.Address)
	}
	if !prof.PodWait.Set || prof.PodWaitStr != "30s" {
		t.Fatalf("expected podwait alias to be captured")
	}
}

func TestParseK8sAliasValidationErrors(t *testing.T) {
	localPortSrc := `### bad
# @k8s pod=api port=8080 local-port=99999
GET http://example.com
`
	doc := Parse("k8s_bad_local_port_alias.http", []byte(localPortSrc))
	if !hasParseMessage(doc.Errors, "invalid @k8s local-port") {
		t.Fatalf("expected local-port parse error, got %v", doc.Errors)
	}

	podWaitSrc := `### bad
# @k8s pod=api port=8080 pod-running-timeout=bad
GET http://example.com
`
	doc = Parse("k8s_bad_podwait_alias.http", []byte(podWaitSrc))
	if !hasParseMessage(doc.Errors, "invalid @k8s pod-running-timeout") {
		t.Fatalf("expected pod-running-timeout parse error, got %v", doc.Errors)
	}
}

func TestParseK8sRequestUseOverride(t *testing.T) {
	src := `### k8s req
# @k8s use=cluster-api pod={{pod_name}} port=8081
GET http://example.com
`
	doc := Parse("k8s_req.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}

	req := doc.Requests[0]
	if req.K8s == nil {
		t.Fatalf("expected k8s spec on request")
	}
	if req.K8s.Use != "cluster-api" {
		t.Fatalf("unexpected use %q", req.K8s.Use)
	}
	if req.K8s.Inline == nil {
		t.Fatalf("expected inline overrides")
	}
	if req.K8s.Inline.Scope != restfile.K8sScopeRequest {
		t.Fatalf("expected request scope inline, got %v", req.K8s.Inline.Scope)
	}
	if req.K8s.Inline.Pod != "{{pod_name}}" {
		t.Fatalf("unexpected inline pod %q", req.K8s.Inline.Pod)
	}
	if req.K8s.Inline.Target != "pod:{{pod_name}}" {
		t.Fatalf("unexpected inline target %q", req.K8s.Inline.Target)
	}
	if req.K8s.Inline.Port != 8081 || req.K8s.Inline.PortStr != "8081" {
		t.Fatalf("unexpected inline port %d (%q)", req.K8s.Inline.Port, req.K8s.Inline.PortStr)
	}
}

func TestParseK8sRequestDefaultsNamespace(t *testing.T) {
	src := `### k8s req
# @k8s pod=api-server port=8080
GET http://example.com
`
	doc := Parse("k8s_req_default_ns.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}

	req := doc.Requests[0]
	if req.K8s == nil || req.K8s.Inline == nil {
		t.Fatalf("expected inline k8s profile")
	}
	if req.K8s.Inline.Namespace != "default" {
		t.Fatalf("expected default namespace, got %q", req.K8s.Inline.Namespace)
	}
}

func TestParseK8sRejectsMissingTarget(t *testing.T) {
	src := `### bad
# @k8s namespace=default
GET http://example.com
`
	doc := Parse("k8s_missing_target.http", []byte(src))
	if !hasParseMessage(doc.Errors, "@k8s requires target and port or use=") {
		t.Fatalf("expected missing target parse error, got %v", doc.Errors)
	}
}

func TestParseK8sAllowsNamedPort(t *testing.T) {
	src := `### bad
# @k8s pod=api-server port=abc
GET http://example.com
`
	doc := Parse("k8s_named_port.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected named port to parse, got errors: %v", doc.Errors)
	}
	if len(doc.Requests) != 1 || doc.Requests[0].K8s == nil || doc.Requests[0].K8s.Inline == nil {
		t.Fatalf("expected inline k8s profile")
	}
	in := doc.Requests[0].K8s.Inline
	if in.Port != 0 || in.PortStr != "abc" {
		t.Fatalf("unexpected named port parse: %d (%q)", in.Port, in.PortStr)
	}
}

func TestParseK8sRejectsMalformedNamedPort(t *testing.T) {
	src := `### bad
# @k8s pod=api-server port=!!!
GET http://example.com
`
	doc := Parse("k8s_bad_named_port.http", []byte(src))
	if !hasParseMessage(doc.Errors, "invalid @k8s port") {
		t.Fatalf("expected bad named port parse error, got %v", doc.Errors)
	}
}

func TestParseK8sRejectsPartialTemplatePort(t *testing.T) {
	src := `### bad
# @k8s pod=api-server port={{port_name
GET http://example.com
`
	doc := Parse("k8s_bad_template_port.http", []byte(src))
	if !hasParseMessage(doc.Errors, "invalid @k8s port") {
		t.Fatalf("expected partial-template port parse error, got %v", doc.Errors)
	}
}

func TestParseK8sRequestIgnoresPersist(t *testing.T) {
	src := `### req
# @k8s pod=api-server port=8080 persist
GET http://example.com
`
	doc := Parse("k8s_req_persist.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.K8s == nil || req.K8s.Inline == nil {
		t.Fatalf("expected inline k8s profile")
	}
	if req.K8s.Inline.Persist.Set {
		t.Fatalf("request persist should be ignored")
	}
	if !hasParseMessage(doc.Warnings, "@k8s request scope ignores persist") {
		t.Fatalf("expected warning for ignored k8s persist, got %v", doc.Warnings)
	}
}

func TestParseK8sGlobalRequiresTarget(t *testing.T) {
	src := `# @k8s global api namespace=default
GET http://example.com
`
	doc := Parse("k8s_global_missing_target.http", []byte(src))
	if !hasParseMessage(doc.Errors, "@k8s global scope requires target and port") {
		t.Fatalf("expected global scope target parse error, got %v", doc.Errors)
	}
	if len(doc.K8s) != 1 {
		t.Fatalf("expected invalid k8s profile marker, got %d", len(doc.K8s))
	}
	prof := doc.K8s[0]
	if !prof.Invalid {
		t.Fatalf("expected invalid k8s profile marker")
	}
	if prof.Name != "api" || prof.Scope != restfile.K8sScopeGlobal || prof.Line != 1 {
		t.Fatalf("unexpected invalid profile marker: %+v", prof)
	}
	if !strings.Contains(prof.Error, "@k8s global scope requires target and port") {
		t.Fatalf("unexpected invalid profile error: %q", prof.Error)
	}
}

func TestParseK8sServiceTarget(t *testing.T) {
	src := `### svc
# @k8s service=api port=http
GET http://example.com
`
	doc := Parse("k8s_service_target.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("unexpected parse errors: %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	in := doc.Requests[0].K8s.Inline
	if in == nil {
		t.Fatalf("expected inline k8s profile")
	}
	if in.Target != "service:api" {
		t.Fatalf("unexpected target %q", in.Target)
	}
	if in.Pod != "" {
		t.Fatalf("expected pod empty for service target, got %q", in.Pod)
	}
}

func TestParseK8sRejectsMultipleTargets(t *testing.T) {
	src := `### bad
# @k8s pod=api-0 service=api port=8080
GET http://example.com
`
	doc := Parse("k8s_target_conflict.http", []byte(src))
	if !hasParseMessage(doc.Errors, "multiple @k8s targets specified") {
		t.Fatalf("expected multiple target error, got %v", doc.Errors)
	}
}

func TestParseK8sRejectsConflictingTargetAliases(t *testing.T) {
	src := `### bad
# @k8s service=api svc=api-canary port=8080
GET http://example.com
`
	doc := Parse("k8s_target_alias_conflict.http", []byte(src))
	if !hasParseMessage(doc.Errors, "multiple @k8s targets specified") {
		t.Fatalf("expected alias conflict error, got %v", doc.Errors)
	}
}

func TestParseK8sRejectsUnknownTargetKind(t *testing.T) {
	src := `### bad
# @k8s target=job:api port=8080
GET http://example.com
`
	doc := Parse("k8s_bad_target_kind.http", []byte(src))
	if !hasParseMessage(doc.Errors, "invalid @k8s target") {
		t.Fatalf("expected invalid target parse error, got %v", doc.Errors)
	}
}

func TestParseK8sAndSSHConflict(t *testing.T) {
	src := `### conflict
# @k8s pod=api-server port=8080
# @ssh host=1.2.3.4 user=ops
GET http://example.com
`
	doc := Parse("k8s_ssh_conflict.http", []byte(src))
	if !hasParseMessage(doc.Errors, "@ssh cannot be combined with @k8s on the same request") {
		t.Fatalf("expected conflict parse error, got %v", doc.Errors)
	}
}

func TestParseSSHAndK8sConflict(t *testing.T) {
	src := `### conflict
# @ssh host=1.2.3.4 user=ops
# @k8s pod=api-server port=8080
GET http://example.com
`
	doc := Parse("ssh_k8s_conflict.http", []byte(src))
	if !hasParseMessage(doc.Errors, "@k8s cannot be combined with @ssh on the same request") {
		t.Fatalf("expected conflict parse error, got %v", doc.Errors)
	}
}

func TestParseRequestVarDirectiveVariants(t *testing.T) {
	src := `# @name Vars
# @var simple foo
# @var equals key=value
# @var colon key: value
# @var url https://example.com:8443/path
GET https://example.com
`

	doc := Parse("vars.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Variables) != 4 {
		t.Fatalf("expected 4 request variables, got %d", len(req.Variables))
	}

	vals := make(map[string]string)
	for _, v := range req.Variables {
		vals[v.Name] = v.Value
	}

	checks := map[string]string{
		"simple": "foo",
		"equals": "key=value",
		"colon":  "key: value",
		"url":    "https://example.com:8443/path",
	}
	for name, expected := range checks {
		if vals[name] != expected {
			t.Fatalf("expected %s=%q, got %q", name, expected, vals[name])
		}
	}
}

func TestShorthandBeforeMethodDefaultsToFileScope(t *testing.T) {
	src := `### One
@id abc
GET https://example.com
Header: value
`

	doc := Parse("shorthand-position.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request")
	}
	if len(doc.Variables) != 1 {
		t.Fatalf("expected 1 file variable, got %d", len(doc.Variables))
	}
	v := doc.Variables[0]
	if v.Name != "id" || v.Value != "abc" || v.Scope != restfile.ScopeFile {
		t.Fatalf("unexpected file variable %+v", v)
	}
	if len(doc.Requests[0].Variables) != 0 {
		t.Fatalf("expected no request variables, got %d", len(doc.Requests[0].Variables))
	}
}

func TestUnknownCommentDirectiveDoesNotSwallowTrailingFileVars(t *testing.T) {
	src := `###
# @name FusionAuthLogin
POST https://example.com/login
Content-Type: application/json

{"loginId":"{{admin_email}}"}

###
@hostname=http://localhost:3000
# @hostname=https://staging.example.com
@admin_email=admin@example.com
`

	doc := Parse("unknown-comment-directive.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	if len(doc.Variables) != 2 {
		t.Fatalf("expected 2 file variables, got %d", len(doc.Variables))
	}

	fileVars := map[string]restfile.Variable{}
	for _, v := range doc.Variables {
		fileVars[v.Name] = v
	}
	if v, ok := fileVars["hostname"]; !ok || v.Scope != restfile.ScopeFile {
		t.Fatalf("expected hostname as file variable, got %#v", v)
	}
	if v, ok := fileVars["admin_email"]; !ok || v.Scope != restfile.ScopeFile {
		t.Fatalf("expected admin_email as file variable, got %#v", v)
	}
}

func TestShorthandAfterBodyStaysRequestScoped(t *testing.T) {
	src := `### reset
POST https://example.com
Content-Type: application/json

{"ok":true}
@tail outside
@tail2 outside-2
`

	doc := Parse("shorthand-tail.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Variables) != 2 {
		t.Fatalf("expected 2 request variables, got %d", len(req.Variables))
	}
	if len(doc.Variables) != 0 {
		t.Fatalf("expected no file variables, got %d", len(doc.Variables))
	}
	values := map[string]string{}
	for _, v := range req.Variables {
		if v.Scope != restfile.ScopeRequest {
			t.Fatalf("expected %s to be request scoped, got %v", v.Name, v.Scope)
		}
		values[v.Name] = v.Value
	}
	if values["tail"] != "outside" || values["tail2"] != "outside-2" {
		t.Fatalf("unexpected request vars: %+v", values)
	}
}

func TestShorthandAfterHeaderWithoutBodyStaysRequestScoped(t *testing.T) {
	src := `GET https://example.com

@tail outside
`

	doc := Parse("shorthand-tail-nobody.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Variables) != 1 {
		t.Fatalf("expected 1 request variable, got %d", len(req.Variables))
	}
	if len(doc.Variables) != 0 {
		t.Fatalf("expected no file variables, got %d", len(doc.Variables))
	}
	v := req.Variables[0]
	if v.Name != "tail" || v.Value != "outside" || v.Scope != restfile.ScopeRequest {
		t.Fatalf("unexpected request var %+v", v)
	}
}

func TestTrailingShorthandAfterFirstRequestDoesNotBleedIntoSecond(t *testing.T) {
	src := `### One
GET https://example.com

@fileVar shared

### Two
GET https://example.com/2
@reqVar inside
`

	doc := Parse("shorthand-multi.http", []byte(src))
	if len(doc.Requests) != 2 {
		t.Fatalf("expected 2 requests, got %d", len(doc.Requests))
	}
	req1 := doc.Requests[0]
	req2 := doc.Requests[1]

	if len(doc.Variables) != 0 {
		t.Fatalf("expected no file variables, got %d", len(doc.Variables))
	}

	if len(req1.Variables) != 1 {
		t.Fatalf("expected 1 request var in first request, got %d", len(req1.Variables))
	}
	if req1.Variables[0].Name != "fileVar" || req1.Variables[0].Scope != restfile.ScopeRequest {
		t.Fatalf("unexpected var in first request: %+v", req1.Variables[0])
	}

	if len(req2.Variables) != 1 {
		t.Fatalf("expected 1 request var in second request, got %d", len(req2.Variables))
	}
	if req2.Variables[0].Name != "reqVar" || req2.Variables[0].Scope != restfile.ScopeRequest {
		t.Fatalf("unexpected var in second request: %+v", req2.Variables[0])
	}
}

func TestShorthandRequestScopeExplicit(t *testing.T) {
	src := `### One
@request requestId abc123
GET https://example.com
`

	doc := Parse("shorthand-request.http", []byte(src))
	if len(doc.Variables) != 0 {
		t.Fatalf("expected no file variables, got %d", len(doc.Variables))
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request")
	}
	req := doc.Requests[0]
	if len(req.Variables) != 1 {
		t.Fatalf("expected 1 request variable, got %d", len(req.Variables))
	}
	v := req.Variables[0]
	if v.Name != "requestId" || v.Value != "abc123" {
		t.Fatalf("unexpected request variable %q=%q", v.Name, v.Value)
	}
	if v.Scope != restfile.ScopeRequest {
		t.Fatalf("expected requestId to be request scoped, got %v", v.Scope)
	}
}

func TestExplicitRequestVarNotMovedAfterBody(t *testing.T) {
	src := `POST https://example.com

{}
@request keep me
`

	doc := Parse("shorthand-request-tail.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request")
	}
	req := doc.Requests[0]
	if len(req.Variables) != 1 {
		t.Fatalf("expected 1 request variable, got %d", len(req.Variables))
	}
	if len(doc.Variables) != 0 {
		t.Fatalf("expected no file variables, got %d", len(doc.Variables))
	}
	v := req.Variables[0]
	if v.Name != "keep" || v.Value != "me" || v.Scope != restfile.ScopeRequest {
		t.Fatalf("unexpected request variable %+v", v)
	}
}

func TestParseCaptureDirectiveGlobal(t *testing.T) {
	src := `# @name Capture
# @capture global auth.token {{response.json.json.token}}
GET https://example.com
`

	doc := Parse("capture.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Captures) != 1 {
		t.Fatalf("expected 1 capture, got %d", len(req.Metadata.Captures))
	}
	cap := req.Metadata.Captures[0]
	if cap.Scope != restfile.CaptureScopeGlobal {
		t.Fatalf("expected global capture scope, got %v", cap.Scope)
	}
	if cap.Name != "auth.token" {
		t.Fatalf("expected capture name auth.token, got %q", cap.Name)
	}
	if cap.Expression != "{{response.json.json.token}}" {
		t.Fatalf("unexpected capture expression %q", cap.Expression)
	}
	if cap.Mode != restfile.CaptureExprModeTemplate {
		t.Fatalf("expected template capture mode, got %v", cap.Mode)
	}
	if cap.Line != 2 {
		t.Fatalf("unexpected capture line=%d", cap.Line)
	}
}

func TestParseCaptureDirectiveRSTExpression(t *testing.T) {
	src := `# @name Capture
# @capture global-secret auth.token = response.json.token
GET https://example.com
`

	doc := Parse("capture-rst.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Captures) != 1 {
		t.Fatalf("expected 1 capture, got %d", len(req.Metadata.Captures))
	}
	cap := req.Metadata.Captures[0]
	if cap.Scope != restfile.CaptureScopeGlobal {
		t.Fatalf("expected global capture scope, got %v", cap.Scope)
	}
	if !cap.Secret {
		t.Fatalf("expected secret capture")
	}
	if cap.Name != "auth.token" {
		t.Fatalf("expected capture name auth.token, got %q", cap.Name)
	}
	if cap.Expression != "response.json.token" {
		t.Fatalf("unexpected capture expression %q", cap.Expression)
	}
	if cap.Mode != restfile.CaptureExprModeRTS {
		t.Fatalf("expected RTS capture mode, got %v", cap.Mode)
	}
	if cap.Line != 2 {
		t.Fatalf("unexpected capture line=%d", cap.Line)
	}
}

func TestParseCaptureDirectiveWarnsOnUnknownScope(t *testing.T) {
	src := `# @capture planet auth.token response.json.token
GET https://example.com
`
	doc := Parse("capture-warn-scope.http", []byte(src))
	if len(doc.Warnings) == 0 {
		t.Fatalf("expected warning for invalid capture scope")
	}
	if !hasParseMessage(doc.Warnings, `scope "planet" is invalid`) {
		t.Fatalf("expected scope warning, got %v", doc.Warnings)
	}
}

func TestParseCaptureDirectiveWarnsOnMissingExpression(t *testing.T) {
	src := `# @capture global auth.token
GET https://example.com
`
	doc := Parse("capture-warn-empty.http", []byte(src))
	if len(doc.Warnings) == 0 {
		t.Fatalf("expected warning for missing capture expression")
	}
	if !hasParseMessage(doc.Warnings, "missing expression") {
		t.Fatalf("expected missing-expression warning, got %v", doc.Warnings)
	}
}

func TestParseCaptureDirectiveWarnsOnJSONPathDoubleDot(t *testing.T) {
	src := `# @capture global auth.token response.json..token
GET https://example.com
`
	doc := Parse("capture-warn-json-dotdot.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	if len(doc.Requests[0].Metadata.Captures) != 1 {
		t.Fatalf("expected capture to still parse")
	}
	if !hasParseMessage(doc.Warnings, "double dot after json") {
		t.Fatalf("expected json-path warning, got %v", doc.Warnings)
	}
}

func TestParseCaptureDirectiveDoesNotWarnOnQuotedDoubleDot(t *testing.T) {
	src := `# @capture global note contains("response.json..token", "x")
GET https://example.com
`
	doc := Parse("capture-warn-json-quoted.http", []byte(src))
	if hasParseMessage(doc.Warnings, "double dot after json") {
		t.Fatalf("did not expect double-dot warning for quoted string, got %v", doc.Warnings)
	}
}

func TestParseCaptureDirectiveStrictAllowsTemplateSyntax(t *testing.T) {
	src := `# @setting capture.strict true
# @capture global auth.token {{response.json.token}}
GET https://example.com
`
	doc := Parse("capture-warn-strict.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	if len(doc.Warnings) != 0 {
		t.Fatalf("did not expect warnings, got %v", doc.Warnings)
	}
}

func TestParseCaptureDirectiveStrictDoesNotWarnOnQuotedTemplateMarkers(t *testing.T) {
	src := `# @setting capture.strict true
# @capture global note contains(response.text(), "{{token}}")
GET https://example.com
`
	doc := Parse("capture-strict-rst-quoted.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	if len(doc.Warnings) != 0 {
		t.Fatalf("did not expect warnings for quoted markers, got %v", doc.Warnings)
	}
	cap := doc.Requests[0].Metadata.Captures[0]
	if cap.Mode != restfile.CaptureExprModeRTS {
		t.Fatalf("expected quoted-marker expression to stay RTS mode, got %v", cap.Mode)
	}
}

func TestParseCaptureDirectiveWarnsOnMixedTemplateRTSCall(t *testing.T) {
	src := `# @capture request mixed contains({{name}}, "x")
GET https://example.com
`
	doc := Parse("capture-warn-mixed.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	if !hasParseMessage(doc.Warnings, "mixes template markers with RTS call syntax") {
		t.Fatalf("expected mixed-syntax warning, got %v", doc.Warnings)
	}
	cap := doc.Requests[0].Metadata.Captures[0]
	if cap.Mode != restfile.CaptureExprModeTemplate {
		t.Fatalf("expected template mode for mixed expression, got %v", cap.Mode)
	}
}

func TestParseCaptureDirectiveWarnsOnMixedTemplateRTSSingleArgCall(t *testing.T) {
	src := `# @capture request mixed contains({{name}})
GET https://example.com
`
	doc := Parse("capture-warn-mixed-single-arg.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	if !hasParseMessage(doc.Warnings, "mixes template markers with RTS call syntax") {
		t.Fatalf("expected mixed-syntax warning, got %v", doc.Warnings)
	}
}

func TestParseOAuth2AuthSpec(t *testing.T) {
	spec := parseAuthSpec(
		`oauth2 token_url="https://auth.example.com/token" client_id=my-client client_secret="s3cr3t" scope="read write" grant=password username=jane password=pwd client_auth=body audience=https://api.example.com`,
	)
	if spec == nil {
		t.Fatalf("expected oauth2 spec")
	}
	if spec.Type != "oauth2" {
		t.Fatalf("unexpected auth type %q", spec.Type)
	}
	checks := map[string]string{
		"token_url":     "https://auth.example.com/token",
		"client_id":     "my-client",
		"client_secret": "s3cr3t",
		"scope":         "read write",
		"grant":         "password",
		"username":      "jane",
		"password":      "pwd",
		"client_auth":   "body",
		"audience":      "https://api.example.com",
	}
	for key, expected := range checks {
		if spec.Params[key] != expected {
			t.Fatalf("expected %s=%q, got %q", key, expected, spec.Params[key])
		}
	}
}

func TestParseOAuth2AuthSpecCacheOnly(t *testing.T) {
	spec := parseAuthSpec(`oauth2 cache_key=github`)
	if spec == nil {
		t.Fatalf("expected oauth2 spec for cache-only directive")
	}
	if spec.Type != "oauth2" {
		t.Fatalf("unexpected auth type %q", spec.Type)
	}
	if spec.Params["cache_key"] != "github" {
		t.Fatalf("expected cache_key to be github, got %q", spec.Params["cache_key"])
	}
	if spec.Params["token_url"] != "" {
		t.Fatalf("expected empty token_url, got %q", spec.Params["token_url"])
	}
	if spec.Params["grant"] != "client_credentials" {
		t.Fatalf("expected default grant client_credentials, got %q", spec.Params["grant"])
	}
	if spec.Params["client_auth"] != "basic" {
		t.Fatalf("expected default client_auth basic, got %q", spec.Params["client_auth"])
	}
}

func TestParseCommandAuthSpec(t *testing.T) {
	spec := parseAuthSpec(
		`command argv='["gh","auth","token"]' header=Authorization cache_key=github timeout=5s`,
	)
	if spec == nil {
		t.Fatalf("expected command auth spec")
	}
	if spec.Type != "command" {
		t.Fatalf("unexpected auth type %q", spec.Type)
	}
	checks := map[string]string{
		"argv":      `["gh","auth","token"]`,
		"header":    "Authorization",
		"cache_key": "github",
		"timeout":   "5s",
	}
	for key, expected := range checks {
		if spec.Params[key] != expected {
			t.Fatalf("expected %s=%q, got %q", key, expected, spec.Params[key])
		}
	}
}

func TestParseCommandAuthSpecBareJSONArgv(t *testing.T) {
	spec := parseAuthSpec(
		`command argv=["gh", "auth", "token"] header=Authorization cache_key=github timeout=5s`,
	)
	if spec == nil {
		t.Fatalf("expected command auth spec")
	}
	if spec.Type != "command" {
		t.Fatalf("unexpected auth type %q", spec.Type)
	}
	checks := map[string]string{
		"argv":      `["gh", "auth", "token"]`,
		"header":    "Authorization",
		"cache_key": "github",
		"timeout":   "5s",
	}
	for key, expected := range checks {
		if spec.Params[key] != expected {
			t.Fatalf("expected %s=%q, got %q", key, expected, spec.Params[key])
		}
	}
}

func TestParseCommandAuthSpecCacheOnly(t *testing.T) {
	spec := parseAuthSpec(`command cache_key=github header=X-Token`)
	if spec == nil {
		t.Fatalf("expected command auth spec")
	}
	if spec.Type != "command" {
		t.Fatalf("unexpected auth type %q", spec.Type)
	}
	if spec.Params["cache_key"] != "github" {
		t.Fatalf("expected cache_key github, got %q", spec.Params["cache_key"])
	}
	if spec.Params["argv"] != "" {
		t.Fatalf("expected argv to be empty, got %q", spec.Params["argv"])
	}
}

func TestParseCompareDirective(t *testing.T) {
	src := `# @name Compare
# @compare dev stage prod base=stage
GET https://example.com/health
`

	doc := Parse("compare.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Metadata.Compare == nil {
		t.Fatalf("expected compare metadata")
	}
	spec := req.Metadata.Compare
	expect := []string{"dev", "stage", "prod"}
	if len(spec.Environments) != len(expect) {
		t.Fatalf("unexpected environments: %#v", spec.Environments)
	}
	for idx, env := range expect {
		if spec.Environments[idx] != env {
			t.Fatalf("expected env %q at position %d, got %q", env, idx, spec.Environments[idx])
		}
	}
	if spec.Baseline != "stage" {
		t.Fatalf("expected baseline stage, got %q", spec.Baseline)
	}
}

func TestParseCompareDirectiveErrors(t *testing.T) {
	src := `# @name Compare
# @compare dev dev
GET https://example.com
`

	doc := Parse("compare.http", []byte(src))
	if len(doc.Errors) == 0 {
		t.Fatalf("expected parse errors")
	}
	req := doc.Requests[0]
	if req.Metadata.Compare != nil {
		t.Fatalf("expected compare metadata to be nil on error")
	}
}

func TestParseCompareDirectiveRejectsSharedEnvironment(t *testing.T) {
	src := `# @name Compare
# @compare dev $shared
GET https://example.com
`

	doc := Parse("compare.http", []byte(src))
	if len(doc.Errors) == 0 {
		t.Fatalf("expected parse errors")
	}
	if !hasParseMessage(doc.Errors, "reserved for shared defaults") {
		t.Fatalf("expected reserved-name parse error, got %v", doc.Errors)
	}
	req := doc.Requests[0]
	if req.Metadata.Compare != nil {
		t.Fatalf("expected compare metadata to be nil on error")
	}
}

func TestParseCompareDirectiveRejectsSharedBaseline(t *testing.T) {
	src := `# @name Compare
# @compare dev stage base=$shared
GET https://example.com
`

	doc := Parse("compare.http", []byte(src))
	if len(doc.Errors) == 0 {
		t.Fatalf("expected parse errors")
	}
	if !hasParseMessage(doc.Errors, "reserved for shared defaults") {
		t.Fatalf("expected reserved-name parse error, got %v", doc.Errors)
	}
	req := doc.Requests[0]
	if req.Metadata.Compare != nil {
		t.Fatalf("expected compare metadata to be nil on error")
	}
}

func TestParseMultiLineScripts(t *testing.T) {
	src := `# @name Scripted
# @script pre-request
> const token = vars.get("token");
> request.setHeader("X-Debug", token);

# @script test
> tests["status"] = () => {
>   tests.assert(response.statusCode === 200, "status code");
> };
GET https://example.com/api
`

	doc := Parse("scripted.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Scripts) != 2 {
		t.Fatalf("expected 2 script blocks, got %d", len(req.Metadata.Scripts))
	}
	pre := req.Metadata.Scripts[0]
	if pre.Kind != "pre-request" {
		t.Fatalf("expected pre-request script, got %s", pre.Kind)
	}
	expectedPre := "const token = vars.get(\"token\");\nrequest.setHeader(\"X-Debug\", token);"
	if pre.Body != expectedPre {
		t.Fatalf("unexpected pre-request script body: %q", pre.Body)
	}
	testBlock := req.Metadata.Scripts[1]
	if testBlock.Kind != "test" {
		t.Fatalf("expected test script, got %s", testBlock.Kind)
	}
	if strings.Count(testBlock.Body, "\n") != 2 {
		t.Fatalf("expected multi-line script body, got %q", testBlock.Body)
	}
	for _, fragment := range []string{"tests[\"status\"] = () => {", "tests.assert(response.statusCode === 200, \"status code\");", "};"} {
		if !strings.Contains(testBlock.Body, fragment) {
			t.Fatalf("expected test script body to contain %q, got %q", fragment, testBlock.Body)
		}
	}
}

func TestParseScriptBlockBraces(t *testing.T) {
	src := `# @name Scripted
# @script test
> {%
client.test("ok", function () {
tests.assert(response.statusCode === 200, "ok");
});
%}
GET https://example.com/api
`

	doc := Parse("scriptblock.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Scripts) != 1 {
		t.Fatalf("expected 1 script block, got %d", len(req.Metadata.Scripts))
	}
	script := req.Metadata.Scripts[0]
	if script.Kind != "test" {
		t.Fatalf("expected test script, got %s", script.Kind)
	}
	expected := "client.test(\"ok\", function () {\n" +
		"tests.assert(response.statusCode === 200, \"ok\");\n" +
		"});"
	if script.Body != expected {
		t.Fatalf("unexpected script body: %q", script.Body)
	}
}

func TestParseScriptInlineBraces(t *testing.T) {
	src := `# @script test
> {% client.test("ok"); %}
GET https://example.com/api
`

	doc := Parse("inline-braces.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Scripts) != 1 {
		t.Fatalf("expected 1 script block, got %d", len(req.Metadata.Scripts))
	}
	body := req.Metadata.Scripts[0].Body
	if body != "{% client.test(\"ok\"); %}" {
		t.Fatalf("unexpected inline script body: %q", body)
	}
}

func TestParseScriptBlockMissingEnd(t *testing.T) {
	src := `# @script test
> {%
client.test("ok", function () {});
### Next
GET https://example.com/next
`

	doc := Parse("missing-end.http", []byte(src))
	if len(doc.Errors) == 0 {
		t.Fatalf("expected parse errors")
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	if doc.Requests[0].URL != "https://example.com/next" {
		t.Fatalf("unexpected request url: %q", doc.Requests[0].URL)
	}
	if doc.Errors[0].Message != "script block missing %}" {
		t.Fatalf("unexpected error message: %q", doc.Errors[0].Message)
	}
}

func TestParseScriptBlockUnicodeWhitespaceStart(t *testing.T) {
	src := "# @script test\n" +
		">\u00A0{%\n" +
		"tests.assert(true, \"ok\");\n" +
		"%}\n" +
		"GET https://example.com\n"

	doc := Parse("unicode-start.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	scripts := doc.Requests[0].Metadata.Scripts
	if len(scripts) != 1 {
		t.Fatalf("expected 1 script, got %d", len(scripts))
	}
	if scripts[0].Body != `tests.assert(true, "ok");` {
		t.Fatalf("unexpected script body: %q", scripts[0].Body)
	}
}

func TestParseScriptBlockUnicodeWhitespaceEnd(t *testing.T) {
	src := "# @script test\n" +
		"> {%\n" +
		"tests.assert(true, \"ok\");\n" +
		">\u2003%}\n" +
		"GET https://example.com\n"

	doc := Parse("unicode-end.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	scripts := doc.Requests[0].Metadata.Scripts
	if len(scripts) != 1 {
		t.Fatalf("expected 1 script, got %d", len(scripts))
	}
	if scripts[0].Body != `tests.assert(true, "ok");` {
		t.Fatalf("unexpected script body: %q", scripts[0].Body)
	}
}

func TestParseScriptBlockMarkersInsideBlockComment(t *testing.T) {
	src := `/*
> {%
tests.assert(true, "ignored");
%}
*/
GET https://example.com
`

	doc := Parse("commented-script.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	if len(doc.Requests[0].Metadata.Scripts) != 0 {
		t.Fatalf(
			"expected no scripts from commented block, got %d",
			len(doc.Requests[0].Metadata.Scripts),
		)
	}
}

func TestParseScriptBlockEndWithPrefix(t *testing.T) {
	src := `# @script test
> {%
tests.assert(true, "ok");
> %}
GET https://example.com
`

	doc := Parse("script-end-prefix.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	scripts := doc.Requests[0].Metadata.Scripts
	if len(scripts) != 1 {
		t.Fatalf("expected 1 script, got %d", len(scripts))
	}
	if scripts[0].Body != `tests.assert(true, "ok");` {
		t.Fatalf("unexpected script body: %q", scripts[0].Body)
	}
}

func TestParseScriptBlockEndWithComment(t *testing.T) {
	src := `# @script test
> {%
tests.assert(true, "ok");
%} // end
GET https://example.com
`

	doc := Parse("script-end-comment.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	scripts := doc.Requests[0].Metadata.Scripts
	if len(scripts) != 1 {
		t.Fatalf("expected 1 script, got %d", len(scripts))
	}
	if scripts[0].Body != `tests.assert(true, "ok");` {
		t.Fatalf("unexpected script body: %q", scripts[0].Body)
	}
}

func TestParseScriptFileInclude(t *testing.T) {
	src := `# @name FileScript
# @script test
> < ./scripts/validation.js
GET https://example.com/api
`

	doc := Parse("filescript.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Scripts) != 1 {
		t.Fatalf("expected 1 script block, got %d", len(req.Metadata.Scripts))
	}
	script := req.Metadata.Scripts[0]
	if script.FilePath != "./scripts/validation.js" {
		t.Fatalf("unexpected script file path: %q", script.FilePath)
	}
	if script.Body != "" {
		t.Fatalf("expected script body to be empty for file include, got %q", script.Body)
	}
}

func TestParseScriptFileIncludeWithIndent(t *testing.T) {
	src := `# @script test
>     < ./script.js
GET https://example.com`

	doc := Parse("indent.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if len(req.Metadata.Scripts) != 1 {
		t.Fatalf("expected 1 script block, got %d", len(req.Metadata.Scripts))
	}
	if req.Metadata.Scripts[0].FilePath != "./script.js" {
		t.Fatalf("unexpected script file path: %q", req.Metadata.Scripts[0].FilePath)
	}
}

func TestParseProfileDirective(t *testing.T) {
	src := `### Timed
# @profile count=5 warmup=2 delay=250ms
GET https://example.com/api
`

	doc := Parse("profile.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Metadata.Profile == nil {
		t.Fatalf("expected profile metadata to be parsed")
	}
	prof := req.Metadata.Profile
	if prof.Count != 5 {
		t.Fatalf("expected count=5, got %d", prof.Count)
	}
	if prof.Warmup != 2 {
		t.Fatalf("expected warmup=2, got %d", prof.Warmup)
	}
	if prof.Delay != 250*time.Millisecond {
		t.Fatalf("expected delay=250ms, got %s", prof.Delay)
	}
}

func TestParseBodyExpandDirective(t *testing.T) {
	src := `### ExpandBody
# @body expand
POST https://example.com/api

< ./payload.json
`

	doc := Parse("body-expand.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if !req.Body.Options.ExpandTemplates {
		t.Fatalf("expected expand templates flag to be set")
	}
	if req.Body.FilePath != "./payload.json" {
		t.Fatalf("unexpected file path %q", req.Body.FilePath)
	}
}

func TestParseInlineXMLBody(t *testing.T) {
	src := `POST https://dataaccess.com
Content-Type: text/xml; charset=utf-8

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://xmlsoap.org">
  <soap:Body>
    <NumberToWords xmlns="http://dataaccess.com">
      <ubiNum>500</ubiNum>
    </NumberToWords>
  </soap:Body>
</soap:Envelope>
`

	doc := Parse("soap.http", []byte(src))
	if len(doc.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", doc.Errors)
	}
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "" {
		t.Fatalf("expected inline XML body, got file path %q", req.Body.FilePath)
	}
	if !strings.Contains(req.Body.Text, `<?xml version="1.0" encoding="utf-8"?>`) {
		t.Fatalf("expected XML declaration in body, got %q", req.Body.Text)
	}
	if !strings.Contains(req.Body.Text, `<soap:Envelope xmlns:soap="http://xmlsoap.org">`) {
		t.Fatalf("expected SOAP envelope in body, got %q", req.Body.Text)
	}
}

func TestParseInlineMalformedXMLBody(t *testing.T) {
	src := `### POST http://localhost:8080/server.php
POST http://localhost:8080/server.php
Content-Type: text/xml

<soap:Envelope xmlns:soap=http://schemas.xmlsoap.org/soap/envelope/>
<soap:Body>
  <get_the_infos_ssm/>
</soap:Body>
</soap:Envelope>
`

	doc := Parse("soap-malformed.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "" {
		t.Fatalf("expected inline XML body, got file path %q", req.Body.FilePath)
	}
	if !strings.Contains(
		req.Body.Text,
		`<soap:Envelope xmlns:soap=http://schemas.xmlsoap.org/soap/envelope/>`,
	) {
		t.Fatalf("expected malformed XML to remain body text, got %q", req.Body.Text)
	}
	if !strings.Contains(req.Body.Text, `<get_the_infos_ssm/>`) {
		t.Fatalf("expected self-closing XML tag in body, got %q", req.Body.Text)
	}
}

func TestParseMultiLineXMLStartTagBody(t *testing.T) {
	src := `POST https://example.com/soap
Content-Type: text/xml

<soap:Envelope
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body/>
</soap:Envelope>
`

	doc := Parse("soap-multiline-start.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "" {
		t.Fatalf("expected inline XML body, got file path %q", req.Body.FilePath)
	}
	if !strings.Contains(req.Body.Text, "<soap:Envelope\n") {
		t.Fatalf("expected multiline start tag body, got %q", req.Body.Text)
	}
}

func TestParseCompactAngleBodyStaysInlineText(t *testing.T) {
	src := `POST https://example.com/api

<payload.xml
`

	doc := Parse("body-file-nospace.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "" {
		t.Fatalf("unexpected file path %q", req.Body.FilePath)
	}
	if req.Body.Text != "<payload.xml" {
		t.Fatalf("unexpected inline body text %q", req.Body.Text)
	}
}

func TestParseBodyFileReferencesWithXMLContentType(t *testing.T) {
	src := `### Compact XML body
POST https://example.com/api
Content-Type: application/xml

<payload.xml

### Spaced body file
POST https://example.com/api
Content-Type: text/xml

< ./payload.xml
`

	doc := Parse("body-file-xml-content-type.http", []byte(src))
	if len(doc.Requests) != 2 {
		t.Fatalf("expected 2 requests, got %d", len(doc.Requests))
	}

	if got := strings.TrimSpace(doc.Requests[0].Body.Text); got != "<payload.xml" {
		t.Fatalf("unexpected compact inline body %q", got)
	}
	if got := doc.Requests[0].Body.FilePath; got != "" {
		t.Fatalf("unexpected compact file path %q", got)
	}
	if got := doc.Requests[1].Body.FilePath; got != "./payload.xml" {
		t.Fatalf("unexpected spaced file path %q", got)
	}
}

func TestParsePlainXMLTagWithXMLContentType(t *testing.T) {
	src := `POST https://example.com/api
Content-Type: application/xml

<Invoice
  id="123">
</Invoice>
`

	doc := Parse("plain-xml-tag.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "" {
		t.Fatalf("expected inline XML body, got file path %q", req.Body.FilePath)
	}
	if !strings.Contains(req.Body.Text, "<Invoice\n") {
		t.Fatalf("expected inline XML body, got %q", req.Body.Text)
	}
}

func TestParsePlainAngleNameWithoutContentTypeStaysInlineBody(t *testing.T) {
	src := `POST https://example.com/api

<Invoice
`

	doc := Parse("plain-angle-name.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "" {
		t.Fatalf("expected inline body, got file reference %q", req.Body.FilePath)
	}
	if req.Body.Text != "<Invoice" {
		t.Fatalf("unexpected inline body text %q", req.Body.Text)
	}
}

func TestParseBodyInlineDirectiveForcesAngleBodyText(t *testing.T) {
	src := `# @body expand
# @body inline
POST https://example.com/api

< this is just a string
`

	doc := Parse("inline-angle-string.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "" {
		t.Fatalf("expected inline body, got file path %q", req.Body.FilePath)
	}
	if req.Body.Text != "< this is just a string" {
		t.Fatalf("unexpected inline body text %q", req.Body.Text)
	}
	if !req.Body.Options.ForceInline {
		t.Fatalf("expected ForceInline option")
	}
	if !req.Body.Options.ExpandTemplates {
		t.Fatalf("expected ExpandTemplates option")
	}
}

func TestParseBodyRawDirectiveAliasForcesAngleBodyText(t *testing.T) {
	src := `# @body raw
POST https://example.com/api

< ./payload.xml
`

	doc := Parse("raw-angle-string.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "" {
		t.Fatalf("expected inline body, got file path %q", req.Body.FilePath)
	}
	if req.Body.Text != "< ./payload.xml" {
		t.Fatalf("unexpected inline body text %q", req.Body.Text)
	}
	if !req.Body.Options.ForceInline {
		t.Fatalf("expected ForceInline option")
	}
}

func TestParseBodyInlineDirectiveFalseKeepsFileReference(t *testing.T) {
	src := `# @body inline false
POST https://example.com/api

< ./payload.xml
`

	doc := Parse("inline-angle-false.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "./payload.xml" {
		t.Fatalf("expected body file reference, got %q", req.Body.FilePath)
	}
	if req.Body.Options.ForceInline {
		t.Fatalf("expected ForceInline to remain false")
	}
}

func TestParseBodyInlineDirectiveInvalidValueDoesNotEnable(t *testing.T) {
	src := `# @body inline maybe
POST https://example.com/api

< ./payload.xml
`

	doc := Parse("inline-angle-invalid.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "./payload.xml" {
		t.Fatalf("expected body file reference, got %q", req.Body.FilePath)
	}
	if req.Body.Options.ForceInline {
		t.Fatalf("expected invalid ForceInline value to be ignored")
	}
}

func TestParseExplicitBodyFileReferenceWithGreaterThan(t *testing.T) {
	src := `POST https://example.com/api

< ./payload>v1.xml
`

	doc := Parse("body-file-gt.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "./payload>v1.xml" {
		t.Fatalf("unexpected file path %q", req.Body.FilePath)
	}
	if req.Body.Text != "" {
		t.Fatalf("expected no inline body text, got %q", req.Body.Text)
	}
}

func TestParseBodyFileReferenceStillWinsOverLaterText(t *testing.T) {
	src := `POST https://example.com/api

< ./payload.xml
ignored
`

	doc := Parse("body-file-wins.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "./payload.xml" {
		t.Fatalf("unexpected file path %q", req.Body.FilePath)
	}
	if req.Body.Text != "" {
		t.Fatalf("expected file body to win, got inline body %q", req.Body.Text)
	}
}

func TestParseHTTPBodyAtPrefixedAngleLineStaysInlineText(t *testing.T) {
	src := `POST https://example.com/api

@{payload} < ./not-a-file-reference
`

	doc := Parse("http-at-angle-body.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.FilePath != "" {
		t.Fatalf("expected inline body text, got file path %q", req.Body.FilePath)
	}
	if req.Body.Text != "@{payload} < ./not-a-file-reference" {
		t.Fatalf("unexpected inline body text %q", req.Body.Text)
	}
}

func TestParseWorkflowDirectives(t *testing.T) {
	src := `# @workflow provision-account on-failure=continue
# @description Provision new account flow
# @tag smoke regression
# @step Authenticate using=AuthLogin expect.status="200 OK" expect.statusCode=200
# @step CreateProfile using=CreateUser on-failure=stop vars.request.name={{vars.global.username}} expect.status="201 Created"
# @step Audit using=AuditLog capture=global.auditId

### AuthLogin
GET https://example.com/auth

### CreateUser
POST https://example.com/users

### AuditLog
GET https://example.com/audit
`

	doc := Parse("workflow.http", []byte(src))
	if len(doc.Workflows) != 1 {
		t.Fatalf("expected 1 workflow, got %d", len(doc.Workflows))
	}
	workflow := doc.Workflows[0]
	if workflow.Name != "provision-account" {
		t.Fatalf("unexpected workflow name %q", workflow.Name)
	}
	if workflow.DefaultOnFailure != restfile.WorkflowOnFailureContinue {
		t.Fatalf("expected default on-failure=continue, got %s", workflow.DefaultOnFailure)
	}
	if workflow.Description == "" ||
		!strings.Contains(workflow.Description, "Provision new account flow") {
		t.Fatalf("expected workflow description, got %q", workflow.Description)
	}
	if len(workflow.Tags) != 2 {
		t.Fatalf("expected 2 tags, got %v", workflow.Tags)
	}
	if len(workflow.Steps) != 3 {
		t.Fatalf("expected 3 steps, got %d", len(workflow.Steps))
	}
	step0 := workflow.Steps[0]
	if step0.Using != "AuthLogin" {
		t.Fatalf("expected first step to use AuthLogin, got %q", step0.Using)
	}
	if step0.Expect["status"] != "200 OK" {
		t.Fatalf("expected first step expect.status=200 OK, got %q", step0.Expect["status"])
	}
	if step0.Expect["statuscode"] != "200" {
		t.Fatalf("expected first step expect.statuscode=200, got %q", step0.Expect["statuscode"])
	}
	step1 := workflow.Steps[1]
	if step1.OnFailure != restfile.WorkflowOnFailureStop {
		t.Fatalf("expected second step on-failure=stop, got %s", step1.OnFailure)
	}
	varsKey := "vars.request.name"
	if step1.Vars[varsKey] != "{{vars.global.username}}" {
		t.Fatalf("expected %s override, got %q", varsKey, step1.Vars[varsKey])
	}
	if step1.Expect["status"] != "201 Created" {
		t.Fatalf("expected quoted status value, got %q", step1.Expect["status"])
	}
	step2 := workflow.Steps[2]
	if step2.Options["capture"] != "global.auditId" {
		t.Fatalf("expected capture option propagated, got %v", step2.Options)
	}
	if workflow.LineRange.Start != 1 {
		t.Fatalf("expected workflow start line 1, got %d", workflow.LineRange.Start)
	}
	if workflow.LineRange.End < workflow.LineRange.Start {
		t.Fatalf("invalid workflow line range: %#v", workflow.LineRange)
	}
	if len(doc.Requests) != 3 {
		t.Fatalf("expected 3 requests parsed, got %d", len(doc.Requests))
	}
}

func TestParseWorkflowExpectErrors(t *testing.T) {
	src := `# @workflow demo
# @step EmptyStatus using=Req expect.status=""
# @step EmptyCode using=Req expect.statuscode=
# @step BadCode using=Req expect.statuscode=abc
# @step Mixed using=Req expect.status="200 OK" expect.statuscode=200

### Req
GET https://example.com
`
	doc := Parse("workflow-errors.http", []byte(src))
	if len(doc.Errors) != 3 {
		t.Fatalf("expected 3 parse errors, got %v", doc.Errors)
	}
	for _, want := range []string{
		"expect.status requires a value",
		"expect.statuscode requires a value",
		"expect.statuscode must be an integer",
	} {
		found := false
		for _, err := range doc.Errors {
			if strings.Contains(err.Message, want) {
				found = true
				break
			}
		}
		if !found {
			t.Fatalf("expected parse error containing %q, got %v", want, doc.Errors)
		}
	}
	if len(doc.Workflows) != 1 {
		t.Fatalf("expected 1 workflow, got %d", len(doc.Workflows))
	}
	workflow := doc.Workflows[0]
	if len(workflow.Steps) != 4 {
		t.Fatalf("expected 4 steps, got %d", len(workflow.Steps))
	}
	for i := 0; i < 3; i++ {
		if len(workflow.Steps[i].Expect) != 0 {
			t.Fatalf(
				"expected step %d to have no expectations, got %v",
				i+1,
				workflow.Steps[i].Expect,
			)
		}
	}
	mixed := workflow.Steps[3]
	if mixed.Expect["status"] != "200 OK" {
		t.Fatalf("expected mixed step expect.status=200 OK, got %q", mixed.Expect["status"])
	}
	if mixed.Expect["statuscode"] != "200" {
		t.Fatalf("expected mixed step expect.statuscode=200, got %q", mixed.Expect["statuscode"])
	}
}

func TestParseWorkflowWhenForEach(t *testing.T) {
	src := `# @workflow demo
# @skip-if vars.user.disabled
# @step Guard using=CheckUser
# @for-each item in vars.items
# @step Each using=ProcessItem

### CheckUser
GET https://example.com/user

### ProcessItem
POST https://example.com/items
`
	doc := Parse("workflow-when-each.http", []byte(src))
	if len(doc.Workflows) != 1 {
		t.Fatalf("expected 1 workflow, got %d", len(doc.Workflows))
	}
	wf := doc.Workflows[0]
	if len(wf.Steps) != 2 {
		t.Fatalf("expected 2 steps, got %d", len(wf.Steps))
	}
	st0 := wf.Steps[0]
	if st0.When == nil {
		t.Fatalf("expected first step to have @skip-if condition")
	}
	if !st0.When.Negate || st0.When.Expression != "vars.user.disabled" {
		t.Fatalf("unexpected condition: %+v", st0.When)
	}
	st1 := wf.Steps[1]
	if st1.Kind != restfile.WorkflowStepKindForEach {
		t.Fatalf("expected second step kind for-each, got %s", st1.Kind)
	}
	if st1.ForEach == nil {
		t.Fatalf("expected second step for-each spec")
	}
	if st1.ForEach.Var != "item" || st1.ForEach.Expr != "vars.items" {
		t.Fatalf("unexpected for-each spec: %+v", st1.ForEach)
	}
}

func TestParseBlockComments(t *testing.T) {
	src := `/**
 * @name Blocked
 * @tag smoke regression
 */
GET https://example.org
`

	doc := Parse("block.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Metadata.Name != "Blocked" {
		t.Fatalf("expected name from block comment, got %q", req.Metadata.Name)
	}
	if len(req.Metadata.Tags) != 2 {
		t.Fatalf("expected tags from block comment, got %v", req.Metadata.Tags)
	}
	if req.Metadata.Tags[0] != "smoke" || req.Metadata.Tags[1] != "regression" {
		t.Fatalf("unexpected tags: %v", req.Metadata.Tags)
	}
}

func TestParseScannerError(t *testing.T) {
	line := strings.Repeat("a", maxScanToken+1)
	doc := Parse("long.http", []byte(line))
	if len(doc.Errors) == 0 {
		t.Fatalf("expected parse errors for oversized line")
	}
	if !strings.Contains(doc.Errors[0].Message, "line exceeds") {
		t.Fatalf("unexpected error message: %q", doc.Errors[0].Message)
	}
	if doc.Errors[0].Line != 1 {
		t.Fatalf("expected error at line 1, got %d", doc.Errors[0].Line)
	}
}

func TestParseGraphQLRequest(t *testing.T) {
	src := `# @name GraphQLExample
# @graphql
# @operation FetchUser
POST https://example.com/graphql

query FetchUser($id: ID!) {
  user(id: $id) {
    id
    name
  }
}

# @variables
{
  "id": "123"
}
`

	doc := Parse("graphql.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.GraphQL == nil {
		t.Fatalf("expected GraphQL body")
	}
	gql := req.Body.GraphQL
	if gql.OperationName != "FetchUser" {
		t.Fatalf("unexpected operation name: %q", gql.OperationName)
	}
	if !strings.Contains(gql.Query, "user(id: $id)") {
		t.Fatalf("expected query body, got %q", gql.Query)
	}
	if strings.TrimSpace(gql.Variables) == "" {
		t.Fatalf("expected variables to be captured")
	}
	if !strings.Contains(gql.Variables, "\"id\": \"123\"") {
		t.Fatalf("expected variables json to contain id, got %q", gql.Variables)
	}
	if strings.Contains(gql.Query, "# @variables") {
		t.Fatalf("expected directives stripped from query")
	}
}

func TestParseGraphQLFileRefs(t *testing.T) {
	src := `# @graphql
# @query < ./queries/workspace.graphql
# @variables < ./queries/workspace.variables.json
POST https://example.com/graphql
`

	doc := Parse("graphql-files.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	gql := doc.Requests[0].Body.GraphQL
	if gql == nil {
		t.Fatalf("expected GraphQL body")
	}
	if gql.QueryFile != "./queries/workspace.graphql" {
		t.Fatalf("unexpected query file %q", gql.QueryFile)
	}
	if gql.VariablesFile != "./queries/workspace.variables.json" {
		t.Fatalf("unexpected variables file %q", gql.VariablesFile)
	}
}

func TestParseGraphQLDirectiveFileRefsIgnoreBodyInline(t *testing.T) {
	src := `# @body inline
# @graphql
# @query < ./queries/workspace.graphql
# @variables < ./queries/workspace.variables.json
POST https://example.com/graphql
`

	doc := Parse("graphql-directive-files-inline.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	gql := doc.Requests[0].Body.GraphQL
	if gql == nil {
		t.Fatalf("expected GraphQL body")
	}
	if gql.QueryFile != "./queries/workspace.graphql" {
		t.Fatalf("unexpected query file %q", gql.QueryFile)
	}
	if gql.VariablesFile != "./queries/workspace.variables.json" {
		t.Fatalf("unexpected variables file %q", gql.VariablesFile)
	}
}

func TestParseGraphQLBodyInlineForcesFileLikeLineToText(t *testing.T) {
	src := `# @body inline
# @graphql
POST https://example.com/graphql

< ./queries/workspace.graphql
`

	doc := Parse("graphql-inline-body-file-like.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	gql := doc.Requests[0].Body.GraphQL
	if gql == nil {
		t.Fatalf("expected GraphQL body")
	}
	if gql.QueryFile != "" {
		t.Fatalf("expected inline query text, got query file %q", gql.QueryFile)
	}
	if gql.Query != "< ./queries/workspace.graphql" {
		t.Fatalf("unexpected query text %q", gql.Query)
	}
}

func TestParseGraphQLTemplateHeadAngleLineStaysInlineText(t *testing.T) {
	src := `# @graphql
POST https://example.com/graphql

@{payload} < ./not-a-file-reference
`

	doc := Parse("graphql-template-head-angle.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	gql := doc.Requests[0].Body.GraphQL
	if gql == nil {
		t.Fatalf("expected GraphQL body")
	}
	if gql.QueryFile != "" {
		t.Fatalf("expected inline query text, got query file %q", gql.QueryFile)
	}
	if gql.Query != "@{payload} < ./not-a-file-reference" {
		t.Fatalf("unexpected query text %q", gql.Query)
	}
}

func TestParseGraphQLDisableResetsState(t *testing.T) {
	src := `# @graphql
POST https://example.com/graphql

query First {
  user { id }
}
# @graphql false
# @graphql
query Second {
  user { name }
}
`
	doc := Parse("graphql-reset.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.Body.GraphQL == nil {
		t.Fatalf("expected GraphQL body")
	}
	query := req.Body.GraphQL.Query
	if strings.Contains(query, "First") {
		t.Fatalf("unexpected stale query: %q", query)
	}
	if !strings.Contains(query, "Second") {
		t.Fatalf("expected query to contain second block, got %q", query)
	}
}

func TestParseOptionTokensQuotedValues(t *testing.T) {
	input := `expect.status="201 Created" vars.request.item_name='Workflow Demo Item' note=alpha\ beta message="He said \"hi\"" flag`
	opts := options.Parse(input)

	if got := opts["expect.status"]; got != "201 Created" {
		t.Fatalf("expected expect.status to be '201 Created', got %q", got)
	}
	if got := opts["vars.request.item_name"]; got != "Workflow Demo Item" {
		t.Fatalf("expected vars.request.item_name to keep spaces, got %q", got)
	}
	if got := opts["note"]; got != "alpha beta" {
		t.Fatalf("expected escaped spaces to collapse, got %q", got)
	}
	if got := opts["message"]; got != "He said \"hi\"" {
		t.Fatalf("expected escaped quotes preserved, got %q", got)
	}
	if got := opts["flag"]; got != "true" {
		t.Fatalf("expected bare flag to default to true, got %q", got)
	}
}

func TestParseOptionTokensBareJSONValue(t *testing.T) {
	input := `argv=["gh", "auth", "token"] mode=json`
	opts := options.Parse(input)

	if got := opts["argv"]; got != `["gh", "auth", "token"]` {
		t.Fatalf("expected argv JSON preserved, got %q", got)
	}
	if got := opts["mode"]; got != "json" {
		t.Fatalf("expected mode=json, got %q", got)
	}
}

func TestParseGRPCRequest(t *testing.T) {
	src := `# @name GRPCSample
# @grpc my.pkg.UserService/GetUser
# @grpc-descriptor descriptors/user.pb
# @grpc-plaintext false
# @grpc-metadata authorization: Bearer 123
GRPC localhost:50051

{
  "id": "abc"
}
`

	doc := Parse("grpc.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.GRPC == nil {
		t.Fatalf("expected grpc metadata")
	}
	grpc := req.GRPC
	if grpc.Service != "UserService" || grpc.Method != "GetUser" {
		t.Fatalf("unexpected service/method: %s/%s", grpc.Service, grpc.Method)
	}
	if grpc.Package != "my.pkg" {
		t.Fatalf("unexpected package: %s", grpc.Package)
	}
	if grpc.FullMethod != "/my.pkg.UserService/GetUser" {
		t.Fatalf("unexpected full method: %s", grpc.FullMethod)
	}
	if grpc.DescriptorSet != "descriptors/user.pb" {
		t.Fatalf("unexpected descriptor: %s", grpc.DescriptorSet)
	}
	if grpc.Plaintext {
		t.Fatalf("expected plaintext to be false")
	}
	if !grpc.PlaintextSet {
		t.Fatalf("expected plaintext directive to be marked as set")
	}
	found := false
	for _, pair := range grpc.Metadata {
		if pair.Key == "authorization" && pair.Value == "Bearer 123" {
			found = true
			break
		}
	}
	if !found {
		t.Fatalf("expected metadata to be captured")
	}
	if strings.TrimSpace(grpc.Message) == "" {
		t.Fatalf("expected message body to be captured")
	}
}

func TestParseGRPCMessageFileRef(t *testing.T) {
	src := `# @grpc my.pkg.UserService/GetUser
GRPC localhost:50051

< ./payload.json
`

	doc := Parse("grpc-file.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	grpc := doc.Requests[0].GRPC
	if grpc == nil {
		t.Fatalf("expected grpc request")
	}
	if grpc.MessageFile != "./payload.json" {
		t.Fatalf("unexpected message file %q", grpc.MessageFile)
	}
	if grpc.Message != "" {
		t.Fatalf("expected no inline message, got %q", grpc.Message)
	}
}

func TestParseGRPCBodyInlineForcesFileLikeLineToText(t *testing.T) {
	src := `# @body inline
# @grpc my.pkg.UserService/GetUser
GRPC localhost:50051

< ./payload.json
`

	doc := Parse("grpc-inline-body-file-like.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	grpc := req.GRPC
	if grpc == nil {
		t.Fatalf("expected grpc request")
	}
	if grpc.MessageFile != "" {
		t.Fatalf("expected inline message text, got message file %q", grpc.MessageFile)
	}
	if grpc.Message != "< ./payload.json" {
		t.Fatalf("unexpected inline message %q", grpc.Message)
	}
	if !req.Body.Options.ForceInline {
		t.Fatalf("expected ForceInline option")
	}
}

func TestParseGRPCMetadataRepeats(t *testing.T) {
	src := `# @grpc my.pkg.UserService/GetUser
# @grpc-metadata x-id: one
# @grpc-metadata x-id: two
GRPC localhost:50051
{}`

	doc := Parse("grpc.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	grpc := doc.Requests[0].GRPC
	if grpc == nil {
		t.Fatalf("expected grpc metadata")
	}
	if len(grpc.Metadata) != 2 {
		t.Fatalf("expected 2 metadata entries, got %d", len(grpc.Metadata))
	}
	if grpc.Metadata[0].Key != "x-id" || grpc.Metadata[0].Value != "one" {
		t.Fatalf("unexpected first metadata entry: %#v", grpc.Metadata[0])
	}
	if grpc.Metadata[1].Key != "x-id" || grpc.Metadata[1].Value != "two" {
		t.Fatalf("unexpected second metadata entry: %#v", grpc.Metadata[1])
	}
}

func TestParseGRPCRequestDefaultsPlaintextToUnset(t *testing.T) {
	src := `# @name DefaultPlaintext
# @grpc my.pkg.UserService/GetUser
GRPC localhost:50051
{}
`

	doc := Parse("grpc.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	grpc := doc.Requests[0].GRPC
	if grpc == nil {
		t.Fatalf("expected grpc metadata")
	}
	if grpc.PlaintextSet {
		t.Fatalf("expected plaintext to be unset when directive is missing")
	}
}

func TestParseSSEDirective(t *testing.T) {
	src := `# @name stream
# @sse duration=45s idle=5s max-events=200 max-bytes=64kb
GET https://example.com/events
`

	doc := Parse("sse.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.SSE == nil {
		t.Fatalf("expected SSE metadata to be parsed")
	}
	if req.SSE.Options.TotalTimeout != 45*time.Second {
		t.Fatalf("unexpected total timeout: %v", req.SSE.Options.TotalTimeout)
	}
	if req.SSE.Options.IdleTimeout != 5*time.Second {
		t.Fatalf("unexpected idle timeout: %v", req.SSE.Options.IdleTimeout)
	}
	if req.SSE.Options.MaxEvents != 200 {
		t.Fatalf("unexpected max events: %d", req.SSE.Options.MaxEvents)
	}
	if req.SSE.Options.MaxBytes != 64*1024 {
		t.Fatalf("unexpected max bytes: %d", req.SSE.Options.MaxBytes)
	}
}

func TestParseWebSocketDirectives(t *testing.T) {
	src := `# @name ws
# @websocket timeout=12s idle=6s max-message-bytes=1mb subprotocols=chat,json compression=false
# @ws send Hello world
# @ws send-json {"op":"ping"}
# @ws send-base64 SGVsbG8=
# @ws send-file < data.bin
# @ws ping heartbeat
# @ws wait 2s
# @ws close 1001 going away
GET ws://example.com/socket
`

	doc := Parse("ws.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	if req.WebSocket == nil {
		t.Fatalf("expected websocket metadata")
	}
	ws := req.WebSocket
	if ws.Options.HandshakeTimeout != 12*time.Second {
		t.Fatalf("unexpected handshake timeout: %v", ws.Options.HandshakeTimeout)
	}
	if ws.Options.IdleTimeout != 6*time.Second {
		t.Fatalf("unexpected idle timeout: %v", ws.Options.IdleTimeout)
	}
	if ws.Options.MaxMessageBytes != 1024*1024 {
		t.Fatalf("unexpected max message bytes: %d", ws.Options.MaxMessageBytes)
	}
	if len(ws.Options.Subprotocols) != 2 {
		t.Fatalf("expected 2 subprotocols, got %d", len(ws.Options.Subprotocols))
	}
	if ws.Options.Subprotocols[0] != "chat" || ws.Options.Subprotocols[1] != "json" {
		t.Fatalf("unexpected subprotocol list: %v", ws.Options.Subprotocols)
	}
	if !ws.Options.CompressionSet || ws.Options.Compression {
		t.Fatalf("expected compression flag to be false and explicitly set")
	}
	if len(ws.Steps) != 7 {
		t.Fatalf("expected 7 steps, got %d", len(ws.Steps))
	}
	if ws.Steps[0].Type != restfile.WebSocketStepSendText || ws.Steps[0].Value != "Hello world" {
		t.Fatalf("unexpected first step: %+v", ws.Steps[0])
	}
	if ws.Steps[3].Type != restfile.WebSocketStepSendFile || ws.Steps[3].File != "data.bin" {
		t.Fatalf("unexpected file step: %+v", ws.Steps[3])
	}
	if ws.Steps[5].Type != restfile.WebSocketStepWait || ws.Steps[5].Duration != 2*time.Second {
		t.Fatalf("unexpected wait step: %+v", ws.Steps[5])
	}
	if ws.Steps[6].Type != restfile.WebSocketStepClose || ws.Steps[6].Code != 1001 ||
		ws.Steps[6].Reason != "going away" {
		t.Fatalf("unexpected close step: %+v", ws.Steps[6])
	}
}

func TestParseTraceDirectiveWithBudgets(t *testing.T) {
	src := `# @trace dns<=50ms connect<=120ms total<=400ms tolerance=25ms
GET https://example.com/api
`

	doc := Parse("trace.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	spec := req.Metadata.Trace
	if spec == nil {
		t.Fatalf("expected trace metadata")
	}
	if !spec.Enabled {
		t.Fatalf("expected trace enabled")
	}
	if spec.Budgets.Total != 400*time.Millisecond {
		t.Fatalf("unexpected total budget: %v", spec.Budgets.Total)
	}
	if spec.Budgets.Tolerance != 25*time.Millisecond {
		t.Fatalf("unexpected tolerance: %v", spec.Budgets.Tolerance)
	}
	if spec.Budgets.Phases == nil {
		t.Fatalf("expected phase budgets")
	}
	if spec.Budgets.Phases["dns"] != 50*time.Millisecond {
		t.Fatalf("unexpected dns budget: %v", spec.Budgets.Phases["dns"])
	}
	if spec.Budgets.Phases["connect"] != 120*time.Millisecond {
		t.Fatalf("unexpected connect budget: %v", spec.Budgets.Phases["connect"])
	}
}

func TestParseTraceDirectiveDisabled(t *testing.T) {
	src := `# @trace enabled=false
GET https://example.com/api
`

	doc := Parse("trace-disabled.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	spec := req.Metadata.Trace
	if spec == nil {
		t.Fatalf("expected trace metadata")
	}
	if spec.Enabled {
		t.Fatalf("expected trace disabled")
	}
}

func TestParseTraceDirectiveSkipsEmptyPhaseNames(t *testing.T) {
	src := `# @trace <=50ms =100ms total<=400ms
GET https://example.com/api
`

	doc := Parse("trace-empty-phase.http", []byte(src))
	if len(doc.Requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(doc.Requests))
	}
	req := doc.Requests[0]
	spec := req.Metadata.Trace
	if spec == nil {
		t.Fatalf("expected trace metadata")
	}
	if spec.Budgets.Total != 400*time.Millisecond {
		t.Fatalf("unexpected total budget: %v", spec.Budgets.Total)
	}
	if len(spec.Budgets.Phases) != 0 {
		t.Fatalf("expected no phase budgets, got %v", spec.Budgets.Phases)
	}
}

func TestParseUseDirectiveNoAlias(t *testing.T) {
	src := `# @use ./rts/helpers.rts
GET https://example.com
`
	d := Parse("use.http", []byte(src))
	if len(d.Errors) != 0 {
		t.Fatalf("expected no parse errors, got %v", d.Errors)
	}
	if len(d.Uses) != 1 {
		t.Fatalf("expected 1 use, got %d", len(d.Uses))
	}
	sp := d.Uses[0]
	if sp.Path != "./rts/helpers.rts" {
		t.Fatalf("unexpected use path: %q", sp.Path)
	}
	if sp.Alias != "" {
		t.Fatalf("expected empty alias, got %q", sp.Alias)
	}
}
