// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package cmd

import (
	stdctx "context"
	"encoding/json"
	"io"
	"os"
	"path/filepath"
	"testing"

	"code.gitea.io/tea/modules/config"
	"code.gitea.io/tea/modules/context"
	tea_git "code.gitea.io/tea/modules/git"

	gogit "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/urfave/cli/v3"
)

func TestParseTypedValue(t *testing.T) {
	t.Run("null", func(t *testing.T) {
		v, err := parseTypedValue("null")
		require.NoError(t, err)
		assert.Nil(t, v)
	})

	t.Run("bool true", func(t *testing.T) {
		v, err := parseTypedValue("true")
		require.NoError(t, err)
		assert.Equal(t, true, v)
	})

	t.Run("bool false", func(t *testing.T) {
		v, err := parseTypedValue("false")
		require.NoError(t, err)
		assert.Equal(t, false, v)
	})

	t.Run("integer", func(t *testing.T) {
		v, err := parseTypedValue("42")
		require.NoError(t, err)
		assert.Equal(t, int64(42), v)
	})

	t.Run("float", func(t *testing.T) {
		v, err := parseTypedValue("3.14")
		require.NoError(t, err)
		assert.Equal(t, 3.14, v)
	})

	t.Run("string", func(t *testing.T) {
		v, err := parseTypedValue("hello")
		require.NoError(t, err)
		assert.Equal(t, "hello", v)
	})

	t.Run("JSON array", func(t *testing.T) {
		v, err := parseTypedValue("[1,2,3]")
		require.NoError(t, err)
		assert.Equal(t, []any{float64(1), float64(2), float64(3)}, v)
	})

	t.Run("JSON object", func(t *testing.T) {
		v, err := parseTypedValue(`{"key":"val"}`)
		require.NoError(t, err)
		assert.Equal(t, map[string]any{"key": "val"}, v)
	})

	t.Run("invalid JSON array falls back to string", func(t *testing.T) {
		v, err := parseTypedValue("[not json")
		require.NoError(t, err)
		assert.Equal(t, "[not json", v)
	})

	t.Run("invalid JSON object falls back to string", func(t *testing.T) {
		v, err := parseTypedValue("{not json")
		require.NoError(t, err)
		assert.Equal(t, "{not json", v)
	})

	t.Run("file reference", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "test.txt")
		require.NoError(t, os.WriteFile(tmpFile, []byte("file content\n"), 0o644))
		v, err := parseTypedValue("@" + tmpFile)
		require.NoError(t, err)
		assert.Equal(t, "file content", v)
	})

	t.Run("file reference without trailing newline", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "test.txt")
		require.NoError(t, os.WriteFile(tmpFile, []byte("no newline"), 0o644))
		v, err := parseTypedValue("@" + tmpFile)
		require.NoError(t, err)
		assert.Equal(t, "no newline", v)
	})

	t.Run("empty file reference", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "empty.txt")
		require.NoError(t, os.WriteFile(tmpFile, []byte(""), 0o644))
		v, err := parseTypedValue("@" + tmpFile)
		require.NoError(t, err)
		assert.Equal(t, "", v)
	})

	t.Run("nonexistent file reference", func(t *testing.T) {
		_, err := parseTypedValue("@/nonexistent/file.txt")
		require.Error(t, err)
		assert.Contains(t, err.Error(), "failed to read")
	})

	t.Run("negative integer", func(t *testing.T) {
		v, err := parseTypedValue("-42")
		require.NoError(t, err)
		assert.Equal(t, int64(-42), v)
	})

	t.Run("negative float", func(t *testing.T) {
		v, err := parseTypedValue("-3.14")
		require.NoError(t, err)
		assert.Equal(t, -3.14, v)
	})

	t.Run("scientific notation", func(t *testing.T) {
		v, err := parseTypedValue("1.5e10")
		require.NoError(t, err)
		assert.Equal(t, 1.5e10, v)
	})

	t.Run("empty string", func(t *testing.T) {
		v, err := parseTypedValue("")
		require.NoError(t, err)
		assert.Equal(t, "", v)
	})

	t.Run("string starting with number", func(t *testing.T) {
		v, err := parseTypedValue("123abc")
		require.NoError(t, err)
		assert.Equal(t, "123abc", v)
	})

	t.Run("nested JSON object", func(t *testing.T) {
		v, err := parseTypedValue(`{"user":{"name":"alice","id":1}}`)
		require.NoError(t, err)
		expected := map[string]any{
			"user": map[string]any{
				"name": "alice",
				"id":   float64(1),
			},
		}
		assert.Equal(t, expected, v)
	})

	t.Run("complex JSON array", func(t *testing.T) {
		v, err := parseTypedValue(`[{"id":1},{"id":2}]`)
		require.NoError(t, err)
		expected := []any{
			map[string]any{"id": float64(1)},
			map[string]any{"id": float64(2)},
		}
		assert.Equal(t, expected, v)
	})

	t.Run("quoted string prevents type parsing", func(t *testing.T) {
		v, err := parseTypedValue(`"null"`)
		require.NoError(t, err)
		assert.Equal(t, "null", v)
	})

	t.Run("quoted true becomes string", func(t *testing.T) {
		v, err := parseTypedValue(`"true"`)
		require.NoError(t, err)
		assert.Equal(t, "true", v)
	})

	t.Run("quoted false becomes string", func(t *testing.T) {
		v, err := parseTypedValue(`"false"`)
		require.NoError(t, err)
		assert.Equal(t, "false", v)
	})

	t.Run("quoted number becomes string", func(t *testing.T) {
		v, err := parseTypedValue(`"123"`)
		require.NoError(t, err)
		assert.Equal(t, "123", v)
	})

	t.Run("quoted empty string", func(t *testing.T) {
		v, err := parseTypedValue(`""`)
		require.NoError(t, err)
		assert.Equal(t, "", v)
	})

	t.Run("quoted string with spaces", func(t *testing.T) {
		v, err := parseTypedValue(`"hello world"`)
		require.NoError(t, err)
		assert.Equal(t, "hello world", v)
	})

	t.Run("single quote not treated as quote", func(t *testing.T) {
		v, err := parseTypedValue(`'hello'`)
		require.NoError(t, err)
		assert.Equal(t, "'hello'", v)
	})

	t.Run("unmatched quote at start only", func(t *testing.T) {
		v, err := parseTypedValue(`"hello`)
		require.NoError(t, err)
		assert.Equal(t, `"hello`, v)
	})

	t.Run("unmatched quote at end only", func(t *testing.T) {
		v, err := parseTypedValue(`hello"`)
		require.NoError(t, err)
		assert.Equal(t, `hello"`, v)
	})

	t.Run("quoted string with escaped quote", func(t *testing.T) {
		v, err := parseTypedValue(`"hello \"world\""`)
		require.NoError(t, err)
		assert.Equal(t, `hello "world"`, v)
	})

	t.Run("quoted string with backslash-n", func(t *testing.T) {
		v, err := parseTypedValue(`"line1\nline2"`)
		require.NoError(t, err)
		assert.Equal(t, "line1\nline2", v)
	})

	t.Run("quoted string with tab escape", func(t *testing.T) {
		v, err := parseTypedValue(`"col1\tcol2"`)
		require.NoError(t, err)
		assert.Equal(t, "col1\tcol2", v)
	})

	t.Run("quoted string with backslash", func(t *testing.T) {
		v, err := parseTypedValue(`"path\\to\\file"`)
		require.NoError(t, err)
		assert.Equal(t, `path\to\file`, v)
	})

	t.Run("invalid escape sequence in quoted string", func(t *testing.T) {
		_, err := parseTypedValue(`"bad \z escape"`)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "invalid quoted string")
	})
}

// runApiWithArgs configures a test login, parses the command line, and captures
// the prepared request without opening sockets or making HTTP requests.
func runApiWithArgs(t *testing.T, args []string) (method string, body []byte, err error) {
	t.Helper()

	var capturedMethod string
	var capturedBody []byte

	config.SetConfigForTesting(config.LocalConfig{
		Logins: []config.Login{{
			Name:    "testLogin",
			URL:     "https://gitea.example.com",
			Token:   "test-token",
			User:    "testUser",
			Default: true,
		}},
	})

	// Use the apiFlags factory to get fresh flag instances, avoiding shared
	// hasBeenSet state between tests. Append minimal login/repo flags needed
	// for the test harness.
	cmd := cli.Command{
		Name:                      "api",
		DisableSliceFlagSeparator: true,
		Action: func(_ stdctx.Context, cmd *cli.Command) error {
			ctx, err := context.InitCommand(cmd)
			if err != nil {
				return err
			}
			request, err := prepareAPIRequest(cmd, ctx)
			if err != nil {
				return err
			}
			capturedMethod = request.Method
			capturedBody = append([]byte(nil), request.Body...)
			return nil
		},
		Flags: append(apiFlags(), []cli.Flag{
			&cli.StringFlag{Name: "login", Aliases: []string{"l"}},
			&cli.StringFlag{Name: "repo", Aliases: []string{"r"}},
			&cli.StringFlag{Name: "remote", Aliases: []string{"R"}},
		}...),
		Writer:    io.Discard,
		ErrWriter: io.Discard,
	}

	fullArgs := append([]string{"api", "--login", "testLogin"}, args...)
	runErr := cmd.Run(stdctx.Background(), fullArgs)

	return capturedMethod, capturedBody, runErr
}

func TestApiCommaInFieldValue(t *testing.T) {
	_, body, err := runApiWithArgs(t, []string{"-f", "body=hello, world", "-X", "POST", "/test"})
	require.NoError(t, err)

	var parsed map[string]any
	require.NoError(t, json.Unmarshal(body, &parsed))
	assert.Equal(t, "hello, world", parsed["body"])
}

func TestApiRawDataFlag(t *testing.T) {
	_, body, err := runApiWithArgs(t, []string{"-d", `{"title":"test","body":"hello"}`, "/test"})
	require.NoError(t, err)

	var parsed map[string]any
	require.NoError(t, json.Unmarshal(body, &parsed))
	assert.Equal(t, "test", parsed["title"])
	assert.Equal(t, "hello", parsed["body"])
}

func TestApiDataFieldMutualExclusion(t *testing.T) {
	_, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "-f", "key=val", "/test"})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "--data/-d cannot be combined with --field/-f or --Field/-F")
}

func TestApiMethodAutoDefault(t *testing.T) {
	t.Run("POST when body provided without explicit method", func(t *testing.T) {
		method, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "/test"})
		require.NoError(t, err)
		assert.Equal(t, "POST", method)
	})

	t.Run("explicit method overrides auto-POST", func(t *testing.T) {
		method, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "-X", "PATCH", "/test"})
		require.NoError(t, err)
		assert.Equal(t, "PATCH", method)
	})

	t.Run("GET when no body", func(t *testing.T) {
		method, _, err := runApiWithArgs(t, []string{"/test"})
		require.NoError(t, err)
		assert.Equal(t, "GET", method)
	})
}

func TestApiMultipleFields(t *testing.T) {
	t.Run("multiple -f flags", func(t *testing.T) {
		_, body, err := runApiWithArgs(t, []string{
			"-f", "title=Test Issue",
			"-f", "body=Description here",
			"-X", "POST",
			"/test",
		})
		require.NoError(t, err)

		var parsed map[string]any
		require.NoError(t, json.Unmarshal(body, &parsed))
		assert.Equal(t, "Test Issue", parsed["title"])
		assert.Equal(t, "Description here", parsed["body"])
	})

	t.Run("multiple -F flags with different types", func(t *testing.T) {
		_, body, err := runApiWithArgs(t, []string{
			"-F", "milestone=5",
			"-F", "closed=true",
			"-F", "title=Test",
			"-X", "POST",
			"/test",
		})
		require.NoError(t, err)

		var parsed map[string]any
		require.NoError(t, json.Unmarshal(body, &parsed))
		assert.Equal(t, float64(5), parsed["milestone"])
		assert.Equal(t, true, parsed["closed"])
		assert.Equal(t, "Test", parsed["title"])
	})

	t.Run("combining -f and -F flags", func(t *testing.T) {
		_, body, err := runApiWithArgs(t, []string{
			"-f", "title=Test",
			"-F", "milestone=3",
			"-F", "closed=false",
			"-X", "POST",
			"/test",
		})
		require.NoError(t, err)

		var parsed map[string]any
		require.NoError(t, json.Unmarshal(body, &parsed))
		assert.Equal(t, "Test", parsed["title"])
		assert.Equal(t, float64(3), parsed["milestone"])
		assert.Equal(t, false, parsed["closed"])
	})

	t.Run("-F with JSON array", func(t *testing.T) {
		_, body, err := runApiWithArgs(t, []string{
			"-F", `labels=["bug","enhancement"]`,
			"-X", "POST",
			"/test",
		})
		require.NoError(t, err)

		var parsed map[string]any
		require.NoError(t, json.Unmarshal(body, &parsed))
		assert.Equal(t, []any{"bug", "enhancement"}, parsed["labels"])
	})

	t.Run("-F with JSON object", func(t *testing.T) {
		_, body, err := runApiWithArgs(t, []string{
			"-F", `assignee={"login":"alice","id":123}`,
			"-X", "POST",
			"/test",
		})
		require.NoError(t, err)

		var parsed map[string]any
		require.NoError(t, json.Unmarshal(body, &parsed))
		assignee, ok := parsed["assignee"].(map[string]any)
		require.True(t, ok)
		assert.Equal(t, "alice", assignee["login"])
		assert.Equal(t, float64(123), assignee["id"])
	})

	t.Run("-F with quoted string to prevent type parsing", func(t *testing.T) {
		_, body, err := runApiWithArgs(t, []string{
			"-F", `status="null"`,
			"-F", `enabled="true"`,
			"-F", `count="42"`,
			"-X", "POST",
			"/test",
		})
		require.NoError(t, err)

		var parsed map[string]any
		require.NoError(t, json.Unmarshal(body, &parsed))
		assert.Equal(t, "null", parsed["status"])
		assert.Equal(t, "true", parsed["enabled"])
		assert.Equal(t, "42", parsed["count"])
	})
}

func TestApiDataFromFile(t *testing.T) {
	t.Run("read JSON from file", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "data.json")
		jsonData := `{"title":"From File","body":"File content"}`
		require.NoError(t, os.WriteFile(tmpFile, []byte(jsonData), 0o644))

		_, body, err := runApiWithArgs(t, []string{"-d", "@" + tmpFile, "/test"})
		require.NoError(t, err)

		var parsed map[string]any
		require.NoError(t, json.Unmarshal(body, &parsed))
		assert.Equal(t, "From File", parsed["title"])
		assert.Equal(t, "File content", parsed["body"])
	})

	t.Run("invalid JSON in --data flag", func(t *testing.T) {
		_, _, err := runApiWithArgs(t, []string{"-d", `{invalid json}`, "/test"})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "not valid JSON")
	})

	t.Run("invalid JSON from file includes filename", func(t *testing.T) {
		tmpFile := filepath.Join(t.TempDir(), "bad.json")
		require.NoError(t, os.WriteFile(tmpFile, []byte("not json"), 0o644))

		_, _, err := runApiWithArgs(t, []string{"-d", "@" + tmpFile, "/test"})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "not valid JSON")
		assert.Contains(t, err.Error(), "bad.json")
	})
}

func TestApiErrorHandling(t *testing.T) {
	t.Run("missing endpoint argument", func(t *testing.T) {
		_, _, err := runApiWithArgs(t, []string{})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "endpoint argument required")
	})

	t.Run("invalid field format", func(t *testing.T) {
		_, _, err := runApiWithArgs(t, []string{"-f", "invalidformat", "-X", "POST", "/test"})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "invalid field format")
	})

	t.Run("invalid Field format", func(t *testing.T) {
		_, _, err := runApiWithArgs(t, []string{"-F", "noequalsign", "-X", "POST", "/test"})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "invalid field format")
	})

	t.Run("empty field key with -f", func(t *testing.T) {
		_, _, err := runApiWithArgs(t, []string{"-f", "=value", "-X", "POST", "/test"})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "field key cannot be empty")
	})

	t.Run("empty field key with -F", func(t *testing.T) {
		_, _, err := runApiWithArgs(t, []string{"-F", "=123", "-X", "POST", "/test"})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "field key cannot be empty")
	})

	t.Run("duplicate field key in -f flags", func(t *testing.T) {
		_, _, err := runApiWithArgs(t, []string{"-f", "key=first", "-f", "key=second", "-X", "POST", "/test"})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "duplicate field key")
	})

	t.Run("duplicate field key in -F flags", func(t *testing.T) {
		_, _, err := runApiWithArgs(t, []string{"-F", "key=1", "-F", "key=2", "-X", "POST", "/test"})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "duplicate field key")
	})

	t.Run("duplicate field key across -f and -F flags", func(t *testing.T) {
		_, _, err := runApiWithArgs(t, []string{"-f", "key=string", "-F", "key=123", "-X", "POST", "/test"})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "duplicate field key")
	})
}

func TestExpandPlaceholders(t *testing.T) {
	t.Run("replaces owner and repo", func(t *testing.T) {
		ctx := &context.TeaContext{
			Owner: "myorg",
			Repo:  "myrepo",
		}
		result := expandPlaceholders("/repos/{owner}/{repo}/issues", ctx)
		assert.Equal(t, "/repos/myorg/myrepo/issues", result)
	})

	t.Run("replaces multiple occurrences", func(t *testing.T) {
		ctx := &context.TeaContext{
			Owner: "alice",
			Repo:  "proj",
		}
		result := expandPlaceholders("/repos/{owner}/{repo}/branches?owner={owner}", ctx)
		assert.Equal(t, "/repos/alice/proj/branches?owner=alice", result)
	})

	t.Run("no placeholders returns unchanged", func(t *testing.T) {
		ctx := &context.TeaContext{
			Owner: "alice",
			Repo:  "proj",
		}
		result := expandPlaceholders("/api/v1/version", ctx)
		assert.Equal(t, "/api/v1/version", result)
	})

	t.Run("empty owner and repo produce empty replacements", func(t *testing.T) {
		ctx := &context.TeaContext{}
		result := expandPlaceholders("/repos/{owner}/{repo}", ctx)
		assert.Equal(t, "/repos//", result)
	})

	t.Run("branch left unreplaced when no local repo", func(t *testing.T) {
		ctx := &context.TeaContext{
			Owner: "alice",
			Repo:  "proj",
		}
		result := expandPlaceholders("/repos/{owner}/{repo}/branches/{branch}", ctx)
		assert.Equal(t, "/repos/alice/proj/branches/{branch}", result)
	})

	t.Run("replaces branch from local repo HEAD", func(t *testing.T) {
		tmpDir := t.TempDir()
		repo, err := gogit.PlainInit(tmpDir, false)
		require.NoError(t, err)

		// Create an initial commit so HEAD points to a branch.
		wt, err := repo.Worktree()
		require.NoError(t, err)
		tmpFile := filepath.Join(tmpDir, "init.txt")
		require.NoError(t, os.WriteFile(tmpFile, []byte("init"), 0o644))
		_, err = wt.Add("init.txt")
		require.NoError(t, err)
		_, err = wt.Commit("initial commit", &gogit.CommitOptions{
			Author: &object.Signature{Name: "test", Email: "test@test.com"},
		})
		require.NoError(t, err)

		// Create and checkout a feature branch.
		headRef, err := repo.Head()
		require.NoError(t, err)
		branchRef := plumbing.NewBranchReferenceName("feature/my-branch")
		ref := plumbing.NewHashReference(branchRef, headRef.Hash())
		require.NoError(t, repo.Storer.SetReference(ref))
		require.NoError(t, wt.Checkout(&gogit.CheckoutOptions{Branch: branchRef}))

		ctx := &context.TeaContext{
			Owner:     "alice",
			Repo:      "proj",
			LocalRepo: &tea_git.TeaRepo{Repository: repo},
		}
		result := expandPlaceholders("/repos/{owner}/{repo}/branches/{branch}", ctx)
		assert.Equal(t, "/repos/alice/proj/branches/feature/my-branch", result)
	})
}

func TestIsTextContentType(t *testing.T) {
	tests := []struct {
		name        string
		contentType string
		want        bool
	}{
		{"empty string defaults to text", "", true},
		{"plain text", "text/plain", true},
		{"html", "text/html", true},
		{"json", "application/json", true},
		{"json with charset", "application/json; charset=utf-8", true},
		{"xml", "application/xml", true},
		{"javascript", "application/javascript", true},
		{"yaml", "application/yaml", true},
		{"toml", "application/toml", true},
		{"binary", "application/octet-stream", false},
		{"image", "image/png", false},
		{"pdf", "application/pdf", false},
		{"zip", "application/zip", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := isTextContentType(tt.contentType)
			assert.Equal(t, tt.want, got)
		})
	}
}
