package rules

import (
	"os"
	"strconv"
	"testing"

	gperr "github.com/yusing/goutils/errs"
	expect "github.com/yusing/goutils/testing"
)

func TestParser(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		subject string
		args    []string
		wantErr gperr.Error
	}{
		{
			name:    "basic",
			input:   "rewrite / /foo/bar",
			subject: "rewrite",
			args:    []string{"/", "/foo/bar"},
		},
		{
			name:    "with quotes",
			input:   `error 403 "Forbidden 'foo' 'bar'."`,
			subject: "error",
			args:    []string{"403", "Forbidden 'foo' 'bar'."},
		},
		{
			name:    "with quotes 2",
			input:   `basic_auth "username" "password"`,
			subject: "basic_auth",
			args:    []string{"username", "password"},
		},
		{
			name:    "with escaped",
			input:   `foo bar\ baz bar\r\n\tbaz bar\'\"baz`,
			subject: "foo",
			args:    []string{"bar baz", "bar\r\n\tbaz", `bar'"baz`},
		},
		{
			name:    "empty string",
			input:   `foo '' ""`,
			subject: "foo",
			args:    []string{"", ""},
		},
		{
			name:    "regex_escaped",
			input:   `foo regex(\b\B\s\S\w\W\d\D\$\.\(\)\{\}\|\?\"\')`,
			subject: "foo",
			args:    []string{`regex(\b\B\s\S\w\W\d\D\$\.\(\)\{\}\|\?"')`},
		},
		{
			name:    "quote inside argument",
			input:   `foo "abc 'def'"`,
			subject: "foo",
			args:    []string{"abc 'def'"},
		},
		{
			name:    "quote inside function",
			input:   `foo glob("'/**/to/path'")`,
			subject: "foo",
			args:    []string{"glob(\"'/**/to/path'\")"},
		},
		{
			name:    "quote inside quoted function",
			input:   "foo 'glob(\"`/**/to/path`\")'",
			subject: "foo",
			args:    []string{"glob(\"`/**/to/path`\")"},
		},
		{
			name:    "complex_regex",
			input:   `path !regex("^(_next/static|_next/image|favicon.ico).*$")`,
			subject: "path",
			args:    []string{`!regex("^(_next/static|_next/image|favicon.ico).*$")`},
		},
		{
			name:    "chaos",
			input:   `error 403 "Forbidden "foo" "bar""`,
			subject: "error",
			args:    []string{"403", "Forbidden ", "foo", " ", "bar", ""},
		},
		{
			name:    "chaos2",
			input:   `foo "'bar' 'baz'" abc\ 'foo "bar"'.`,
			subject: "foo",
			args:    []string{"'bar' 'baz'", "abc ", `foo "bar"`, "."},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			subject, args, err := parse(tt.input)
			if tt.wantErr != nil {
				expect.ErrorIs(t, tt.wantErr, err)
				return
			}
			// t.Log(subject, args, err)
			expect.NoError(t, err)
			expect.Equal(t, subject, tt.subject)
			expect.Equal(t, args, tt.args)
		})
	}
	t.Run("env substitution", func(t *testing.T) {
		// Set up test environment variables
		os.Setenv("CLOUDFLARE_API_KEY", "test-api-key-123")
		os.Setenv("DOMAIN", "example.com")
		defer func() {
			os.Unsetenv("CLOUDFLARE_API_KEY")
			os.Unsetenv("DOMAIN")
		}()

		tests := []struct {
			name    string
			input   string
			subject string
			args    []string
			wantErr string
		}{
			{
				name:    "simple env var",
				input:   `error 403 "Forbidden: ${CLOUDFLARE_API_KEY}"`,
				subject: "error",
				args:    []string{"403", "Forbidden: test-api-key-123"},
			},
			{
				name:    "multiple env vars",
				input:   `forward https://${DOMAIN}/api`,
				subject: "forward",
				args:    []string{"https://example.com/api"},
			},
			{
				name:    "env var with other text",
				input:   `auth "user-${DOMAIN}-admin" "password"`,
				subject: "auth",
				args:    []string{"user-example.com-admin", "password"},
			},
			{
				name:    "non-existent env var",
				input:   `error 404 "${NON_EXISTENT}"`,
				wantErr: ErrEnvVarNotFound.Error(),
			},
			{
				name:    "escaped",
				input:   `error 404 "$${NON_EXISTENT}"`,
				subject: "error",
				args:    []string{"404", "${NON_EXISTENT}"},
			},
		}

		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				subject, args, err := parse(tt.input)
				if tt.wantErr != "" {
					expect.ErrorContains(t, err, tt.wantErr)
					return
				}
				expect.NoError(t, err)
				expect.Equal(t, subject, tt.subject)
				expect.Equal(t, args, tt.args)
			})
		}
	})
	t.Run("unterminated quotes", func(t *testing.T) {
		tests := []string{
			`error 403 "Forbidden 'foo' 'bar'`,
			`error 403 "Forbidden 'foo 'bar'`,
			`error 403 "Forbidden foo "bar'"`,
		}
		for i, test := range tests {
			t.Run(strconv.Itoa(i), func(t *testing.T) {
				_, _, err := parse(test)
				expect.ErrorIs(t, ErrUnterminatedQuotes, err)
			})
		}
	})

	t.Run("negated", func(t *testing.T) {
		test := `!error 403 "Forbidden"`
		subject, args, err := parse(test)
		expect.NoError(t, err)
		expect.Equal(t, subject, "!error")
		expect.Equal(t, args, []string{"403", "Forbidden"})
	})
}

func TestFullParse(t *testing.T) {
	input := `
- name: login page
  on: path /login
  do: pass
- name: require auth
  on: path !regex("^(_next/static|_next/image|favicon.ico).*$")
  do: require_auth
- name: redirect to login
  on: status 401 | status 403
  do: proxy /login
- name: proxy to backend
  on: path glob("/api/v1/*")
  do: proxy http://localhost:8999/
- name: proxy to backend (old /auth)
  on: path glob("/auth/*")
  do: proxy http://localhost:8999/api/v1/`

	var rules Rules
	err := parseRules(input, &rules)
	expect.NoError(t, err)
	expect.Equal(t, len(rules), 5)
	expect.Equal(t, rules[0].Name, "login page")
	expect.Equal(t, rules[0].On.String(), "path /login")
	expect.Equal(t, rules[0].Do.String(), "pass")
	expect.Equal(t, rules[1].Name, "require auth")
	expect.Equal(t, rules[1].On.String(), `path !regex("^(_next/static|_next/image|favicon.ico).*$")`)
	expect.Equal(t, rules[1].Do.String(), "require_auth")
	expect.Equal(t, rules[2].Name, "redirect to login")
	expect.Equal(t, rules[2].On.String(), "status 401 | status 403")
	expect.Equal(t, rules[2].Do.String(), "proxy /login")
	expect.Equal(t, rules[3].Name, "proxy to backend")
	expect.Equal(t, rules[3].On.String(), `path glob("/api/v1/*")`)
	expect.Equal(t, rules[3].Do.String(), "proxy http://localhost:8999/")
	expect.Equal(t, rules[4].Name, "proxy to backend (old /auth)")
	expect.Equal(t, rules[4].On.String(), `path glob("/auth/*")`)
	expect.Equal(t, rules[4].Do.String(), "proxy http://localhost:8999/api/v1/")
}

func BenchmarkParser(b *testing.B) {
	const input = `error 403 "Forbidden "foo" "bar""\ baz`
	for b.Loop() {
		_, _, err := parse(input)
		if err != nil {
			b.Fatal(err)
		}
	}
}
