package exec

import (
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
)

func TestApiclientProfileAdapter(t *testing.T) {
	tmpDir := t.TempDir()
	png := imageFixture("image/png")
	imgPath := filepath.Join(tmpDir, "ok.png")
	if err := os.WriteFile(imgPath, png, 0o644); err != nil {
		t.Fatal(err)
	}

	var gotRequest struct {
		method, path, auth, field, filename, contentType string
		blob                                             []byte
	}
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotRequest.method = r.Method
		gotRequest.path = r.URL.EscapedPath()
		gotRequest.auth = r.Header.Get("Authorization")
		reader, err := r.MultipartReader()
		if err != nil {
			t.Errorf("multipart reader: %v", err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		part, err := reader.NextPart()
		if err != nil {
			t.Errorf("multipart part: %v", err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		gotRequest.field = part.FormName()
		gotRequest.filename = part.FileName()
		gotRequest.contentType = part.Header.Get("Content-Type")
		gotRequest.blob, _ = io.ReadAll(part)
		io.WriteString(w, `{"avatarUrl":"https://x/a.png"}`)
	}))
	defer srv.Close()
	mkRemote := func(workDir string) profileRemote {
		return profileRemote{
			client:  &apiclient.Client{ServerURL: srv.URL, AgentID: "ag", AuthToken: "tok", RouteWorkspaceID: "ws", HTTPClient: srv.Client()},
			workDir: workDir,
		}
	}

	t.Run("valid PNG within workdir", func(t *testing.T) {
		gotRequest = struct {
			method, path, auth, field, filename, contentType string
			blob                                             []byte
		}{}
		remote := mkRemote(tmpDir)
		result, err := remote.setAvatar(context.Background(), "ok.png")
		if err != nil {
			t.Fatal(err)
		}
		if result.AvatarURL != "https://x/a.png" {
			t.Errorf("result: %+v", result)
		}
		if gotRequest.method != http.MethodPost ||
			gotRequest.path != "/api/v1/workspaces/ws/agents/ag/runtime/avatar" ||
			gotRequest.auth != "Bearer tok" ||
			gotRequest.field != "file" ||
			gotRequest.filename != "ok.png" ||
			gotRequest.contentType != "image/png" ||
			string(gotRequest.blob) != string(png) {
			t.Errorf("multipart request: %+v blob=%x", gotRequest, gotRequest.blob)
		}
	})
	t.Run("missing workdir -> permission_denied", func(t *testing.T) {
		remote := mkRemote("")
		_, err := remote.setAvatar(context.Background(), "ok.png")
		errIsCode(t, err, CodePermissionDenied)
	})
	t.Run("path outside workdir -> permission_denied", func(t *testing.T) {
		remote := mkRemote(tmpDir)
		_, err := remote.setAvatar(context.Background(), "/etc/passwd")
		errIsCode(t, err, CodePermissionDenied)
	})
	t.Run("traversal rejected", func(t *testing.T) {
		remote := mkRemote(tmpDir)
		_, err := remote.setAvatar(context.Background(), "../outside.png")
		errIsCode(t, err, CodePermissionDenied)
	})
	t.Run("symlink rejected", func(t *testing.T) {
		linkPath := filepath.Join(tmpDir, "link.png")
		if err := os.Symlink(imgPath, linkPath); err != nil {
			t.Fatal(err)
		}
		remote := mkRemote(tmpDir)
		_, err := remote.setAvatar(context.Background(), "link.png")
		errIsCode(t, err, CodePermissionDenied)

		outside := filepath.Join(t.TempDir(), "outside.png")
		if err := os.WriteFile(outside, png, 0o644); err != nil {
			t.Fatal(err)
		}
		linkOut := filepath.Join(tmpDir, "linkout.png")
		if err := os.Symlink(outside, linkOut); err != nil {
			t.Fatal(err)
		}
		_, err = remote.setAvatar(context.Background(), "linkout.png")
		errIsCode(t, err, CodePermissionDenied)
	})
	t.Run("resolver to open symlink swap rejected", func(t *testing.T) {
		swapPath := filepath.Join(tmpDir, "swap.png")
		if err := os.WriteFile(swapPath, png, 0o644); err != nil {
			t.Fatal(err)
		}
		outside := filepath.Join(t.TempDir(), "outside.png")
		if err := os.WriteFile(outside, png, 0o644); err != nil {
			t.Fatal(err)
		}
		remote := mkRemote(tmpDir)
		remote.beforeSecureOpen = func(path string) {
			if err := os.Remove(path); err != nil {
				t.Fatal(err)
			}
			if err := os.Symlink(outside, path); err != nil {
				t.Fatal(err)
			}
		}
		_, err := remote.setAvatar(context.Background(), "swap.png")
		errIsCode(t, err, CodePermissionDenied)
	})
	t.Run("resolver to open intermediate directory swap rejected", func(t *testing.T) {
		inside := filepath.Join(tmpDir, "inside")
		if err := os.Mkdir(inside, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(inside, "avatar.png"), png, 0o644); err != nil {
			t.Fatal(err)
		}
		outside := t.TempDir()
		if err := os.WriteFile(filepath.Join(outside, "avatar.png"), append(imageFixture("image/png"), []byte("outside-secret")...), 0o644); err != nil {
			t.Fatal(err)
		}
		remote := mkRemote(tmpDir)
		remote.beforeSecureOpen = func(string) {
			if err := os.Rename(inside, inside+".original"); err != nil {
				t.Fatal(err)
			}
			if err := os.Symlink(outside, inside); err != nil {
				t.Fatal(err)
			}
		}
		_, err := remote.setAvatar(context.Background(), "inside/avatar.png")
		errIsCode(t, err, CodePermissionDenied)
	})
	t.Run("directory rejected", func(t *testing.T) {
		remote := mkRemote(tmpDir)
		_, err := remote.setAvatar(context.Background(), ".")
		errIsCode(t, err, CodePermissionDenied)
	})
	t.Run("missing file preserves not_found", func(t *testing.T) {
		remote := mkRemote(tmpDir)
		_, err := remote.setAvatar(context.Background(), "missing.png")
		errIsCode(t, err, CodeNotFound)
	})
	t.Run("size boundary", func(t *testing.T) {
		exact := append(imageFixture("image/png"), make([]byte, maxAvatarBytes-int64(len(imageFixture("image/png"))))...)
		if err := os.WriteFile(filepath.Join(tmpDir, "exact.png"), exact, 0o644); err != nil {
			t.Fatal(err)
		}
		remote := mkRemote(tmpDir)
		if _, err := remote.setAvatar(context.Background(), "exact.png"); err != nil {
			t.Fatalf("exact max rejected: %v", err)
		}
		if err := os.WriteFile(filepath.Join(tmpDir, "large.png"), append(exact, 0), 0o644); err != nil {
			t.Fatal(err)
		}
		_, err := remote.setAvatar(context.Background(), "large.png")
		errIsCode(t, err, CodeValidation)
	})
	t.Run("four image formats accepted and spoof rejected", func(t *testing.T) {
		for _, contentType := range []string{"image/jpeg", "image/png", "image/gif", "image/webp"} {
			ext := strings.TrimPrefix(contentType, "image/")
			path := "valid." + ext
			if err := os.WriteFile(filepath.Join(tmpDir, path), imageFixture(contentType), 0o644); err != nil {
				t.Fatal(err)
			}
			remote := mkRemote(tmpDir)
			if _, err := remote.setAvatar(context.Background(), path); err != nil {
				t.Errorf("%s rejected: %v", contentType, err)
			}
		}
		if err := os.WriteFile(filepath.Join(tmpDir, "spoof.png"), []byte("plain text"), 0o644); err != nil {
			t.Fatal(err)
		}
		remote := mkRemote(tmpDir)
		_, err := remote.setAvatar(context.Background(), "spoof.png")
		errIsCode(t, err, CodeValidation)
		if err := os.WriteFile(filepath.Join(tmpDir, "unsupported.bmp"), append([]byte{'B', 'M'}, make([]byte, 510)...), 0o644); err != nil {
			t.Fatal(err)
		}
		_, err = remote.setAvatar(context.Background(), "unsupported.bmp")
		errIsCode(t, err, CodeValidation)
	})
	t.Run("non-image rejected", func(t *testing.T) {
		txtPath := filepath.Join(tmpDir, "notimg.txt")
		os.WriteFile(txtPath, []byte("hello"), 0o644)
		remote := mkRemote(tmpDir)
		_, err := remote.setAvatar(context.Background(), "notimg.txt")
		errIsCode(t, err, CodeValidation)
	})
	t.Run("runtime gates fail before request", func(t *testing.T) {
		for _, client := range []*apiclient.Client{
			nil,
			{RouteWorkspaceID: "ws", AgentID: "ag", AuthToken: "tok"},
			{RouteWorkspaceID: "ws", HTTPClient: srv.Client()},
			{AgentID: "ag", AuthToken: "tok", HTTPClient: srv.Client()},
		} {
			remote := profileRemote{client: client, workDir: tmpDir}
			if _, err := remote.setAvatar(context.Background(), "ok.png"); err == nil {
				t.Fatalf("client %+v passed runtime gate", client)
			}
		}
	})
}

func imageFixture(contentType string) []byte {
	switch contentType {
	case "image/jpeg":
		return append([]byte{0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 'J', 'F', 'I', 'F', 0x00}, make([]byte, 32)...)
	case "image/png":
		return append([]byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}, make([]byte, 32)...)
	case "image/gif":
		return append([]byte("GIF89a"), make([]byte, 32)...)
	case "image/webp":
		return append([]byte("RIFF\x24\x00\x00\x00WEBPVP8 "), make([]byte, 32)...)
	default:
		panic("unsupported fixture: " + contentType)
	}
}
