package workflow_test

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"workflow"
)

// ────────────────────────────────────────────────────────────────────────────
//  LocalFileAdapter tests
// ────────────────────────────────────────────────────────────────────────────

func TestLocalFileAdapter_WriteRead(t *testing.T) {
	root := t.TempDir()
	a, err := workflow.NewLocalFileAdapter(root, "proj1")
	if err != nil {
		t.Fatalf("NewLocalFileAdapter: %v", err)
	}
	ctx := context.Background()

	const path = "/Output/hello.txt"
	const content = "hello world"

	if err := a.Write(ctx, path, []byte(content), workflow.WriteModeOverwrite); err != nil {
		t.Fatalf("Write: %v", err)
	}
	got, err := a.Read(ctx, path)
	if err != nil {
		t.Fatalf("Read: %v", err)
	}
	if string(got) != content {
		t.Errorf("Read = %q, want %q", got, content)
	}
}

func TestLocalFileAdapter_Exists(t *testing.T) {
	root := t.TempDir()
	a, err := workflow.NewLocalFileAdapter(root, "proj1")
	if err != nil {
		t.Fatalf("NewLocalFileAdapter: %v", err)
	}
	ctx := context.Background()

	exists, err := a.Exists(ctx, "/Output/nope.txt")
	if err != nil {
		t.Fatalf("Exists: %v", err)
	}
	if exists {
		t.Error("expected file to not exist")
	}

	_ = a.Write(ctx, "/Output/nope.txt", []byte("x"), workflow.WriteModeOverwrite)

	exists, err = a.Exists(ctx, "/Output/nope.txt")
	if err != nil {
		t.Fatalf("Exists after write: %v", err)
	}
	if !exists {
		t.Error("expected file to exist after write")
	}
}

func TestLocalFileAdapter_WriteModeFailIfExists(t *testing.T) {
	root := t.TempDir()
	a, _ := workflow.NewLocalFileAdapter(root, "proj1")
	ctx := context.Background()

	if err := a.Write(ctx, "/f.txt", []byte("v1"), workflow.WriteModeOverwrite); err != nil {
		t.Fatalf("first write: %v", err)
	}
	err := a.Write(ctx, "/f.txt", []byte("v2"), workflow.WriteModeFailIfExists)
	if err == nil {
		t.Fatal("expected FileExistsError, got nil")
	}
	var fee *workflow.FileExistsError
	if !errors.As(err, &fee) {
		t.Fatalf("expected FileExistsError, got %T: %v", err, err)
	}
	// Content should be unchanged
	got, _ := a.Read(ctx, "/f.txt")
	if string(got) != "v1" {
		t.Errorf("content should be v1, got %q", got)
	}
}

func TestLocalFileAdapter_WriteModeAppend(t *testing.T) {
	root := t.TempDir()
	a, _ := workflow.NewLocalFileAdapter(root, "proj1")
	ctx := context.Background()

	_ = a.Write(ctx, "/log.txt", []byte("line1\n"), workflow.WriteModeOverwrite)
	_ = a.Write(ctx, "/log.txt", []byte("line2\n"), workflow.WriteModeAppend)
	got, _ := a.Read(ctx, "/log.txt")
	if string(got) != "line1\nline2\n" {
		t.Errorf("append result = %q, want %q", got, "line1\nline2\n")
	}
}

func TestLocalFileAdapter_WriteModePrepend(t *testing.T) {
	root := t.TempDir()
	a, _ := workflow.NewLocalFileAdapter(root, "proj1")
	ctx := context.Background()

	_ = a.Write(ctx, "/doc.txt", []byte("world"), workflow.WriteModeOverwrite)
	_ = a.Write(ctx, "/doc.txt", []byte("hello "), workflow.WriteModePrepend)
	got, _ := a.Read(ctx, "/doc.txt")
	if string(got) != "hello world" {
		t.Errorf("prepend result = %q, want %q", got, "hello world")
	}
}

func TestLocalFileAdapter_List(t *testing.T) {
	root := t.TempDir()
	a, _ := workflow.NewLocalFileAdapter(root, "proj1")
	ctx := context.Background()

	files := []string{"/Output/a.txt", "/Output/b.txt", "/Other/c.md"}
	for _, f := range files {
		_ = a.Write(ctx, f, []byte("x"), workflow.WriteModeOverwrite)
	}

	got, err := a.List(ctx, "Output/*")
	if err != nil {
		t.Fatalf("List: %v", err)
	}
	if len(got) != 2 {
		t.Errorf("List(Output/*) = %v, want 2 items", got)
	}
	for _, p := range got {
		if !strings.HasPrefix(p, "/Output/") {
			t.Errorf("path %q should start with /Output/", p)
		}
	}
}

func TestLocalFileAdapter_GIDIsolation(t *testing.T) {
	root := t.TempDir()
	a1, _ := workflow.NewLocalFileAdapter(root, "proj-A")
	a2, _ := workflow.NewLocalFileAdapter(root, "proj-B")
	ctx := context.Background()

	_ = a1.Write(ctx, "/file.txt", []byte("from A"), workflow.WriteModeOverwrite)

	exists, _ := a2.Exists(ctx, "/file.txt")
	if exists {
		t.Error("proj-B should not see proj-A's file")
	}

	_, err := a2.Read(ctx, "/file.txt")
	if err == nil {
		t.Error("proj-B reading proj-A file should fail")
	}
}

func TestLocalFileAdapter_PathTraversalBlocked(t *testing.T) {
	root := t.TempDir()
	a, _ := workflow.NewLocalFileAdapter(root, "proj1")
	ctx := context.Background()

	err := a.Write(ctx, "../../etc/passwd", []byte("evil"), workflow.WriteModeOverwrite)
	if err == nil {
		t.Error("path traversal should be rejected")
	}
}

func TestLocalFileAdapter_ReadNotFound(t *testing.T) {
	root := t.TempDir()
	a, _ := workflow.NewLocalFileAdapter(root, "proj1")
	ctx := context.Background()

	_, err := a.Read(ctx, "/nonexistent.txt")
	if err == nil {
		t.Fatal("expected FileNotFoundError")
	}
	var fnf *workflow.FileNotFoundError
	if !errors.As(err, &fnf) {
		t.Fatalf("expected FileNotFoundError, got %T: %v", err, err)
	}
}

func TestLocalFileAdapter_WorkspaceDir(t *testing.T) {
	root := t.TempDir()
	a, _ := workflow.NewLocalFileAdapter(root, "my-gid")

	// WorkspaceDir should be root/gid
	absRoot, _ := filepath.Abs(root)
	expected := filepath.Join(absRoot, "my-gid")
	if a.WorkspaceDir() != expected {
		t.Errorf("WorkspaceDir = %q, want %q", a.WorkspaceDir(), expected)
	}
}

func TestLocalFileAdapter_AutoCreatesParentDirs(t *testing.T) {
	root := t.TempDir()
	a, _ := workflow.NewLocalFileAdapter(root, "proj1")
	ctx := context.Background()

	// Deep nested path — parent dirs should be created
	err := a.Write(ctx, "/deep/nested/path/file.txt", []byte("data"), workflow.WriteModeOverwrite)
	if err != nil {
		t.Fatalf("Write nested path: %v", err)
	}
	absRoot, _ := filepath.Abs(root)
	expected := filepath.Join(absRoot, "proj1", "deep", "nested", "path", "file.txt")
	if _, err := os.Stat(expected); err != nil {
		t.Errorf("expected file at %s: %v", expected, err)
	}
}

// ────────────────────────────────────────────────────────────────────────────
//  WorkspaceFileAdapter tests (mock HTTP server)
// ────────────────────────────────────────────────────────────────────────────

// mockWorkspaceServer builds an httptest.Server that handles the four
// workspace API endpoints used by WorkspaceFileAdapter.
type mockWorkspaceServer struct {
	// files maps path → content (version=1 if present)
	files map[string]string
	// cookie expected in requests (without "ih5bearer=" prefix)
	cookie string
}

func (m *mockWorkspaceServer) handler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	// Check cookie
	if m.cookie != "" {
		expected := "ih5bearer=" + m.cookie
		if r.Header.Get("Cookie") != expected {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
	}

	// Parse body
	var body map[string]interface{}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}
	path, _ := body["path"].(string)

	// Route
	endpoint := filepath.Base(r.URL.Path)
	switch endpoint {
	case "createFile":
		if _, exists := m.files[path]; !exists {
			m.files[path] = "" // mark as created (version=0)
		}
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintf(w, `{"status":0}`)

	case "writeFile":
		content, _ := body["content"].(string)
		m.files[path] = content
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintf(w, `{"gid":"test","path":%q,"version":1}`, path)

	case "readFile":
		content, exists := m.files[path]
		if !exists || content == "" {
			http.Error(w, "not found", http.StatusNotFound)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintf(w, `{"version":1,"status":0,"content":%s}`, jsonStr(content))

	case "listFile":
		type fileEntry struct {
			Path    string `json:"path"`
			Version int    `json:"version"`
		}
		var entries []fileEntry
		for p, c := range m.files {
			v := 0
			if c != "" {
				v = 1
			}
			entries = append(entries, fileEntry{Path: p, Version: v})
		}
		out, _ := json.Marshal(map[string]interface{}{"files": entries})
		w.Header().Set("Content-Type", "application/json")
		w.Write(out)

	default:
		http.Error(w, "unknown endpoint: "+endpoint, http.StatusNotFound)
	}
}

func jsonStr(s string) string {
	b, _ := json.Marshal(s)
	return string(b)
}

func newMockWorkspaceServer(t *testing.T, token string) (*httptest.Server, *mockWorkspaceServer) {
	t.Helper()
	mock := &mockWorkspaceServer{
		files:  make(map[string]string),
		cookie: token,
	}
	srv := httptest.NewServer(http.HandlerFunc(mock.handler))
	t.Cleanup(srv.Close)
	return srv, mock
}

func newWorkspaceAdapter(t *testing.T, srv *httptest.Server, token, gid string) *workflow.WorkspaceFileAdapter {
	t.Helper()
	a, err := workflow.NewWorkspaceFileAdapter(workflow.WorkspaceFileConfig{
		BaseURL: srv.URL,
		GID:     gid,
		Cookie:  "ih5bearer=" + token,
	})
	if err != nil {
		t.Fatalf("NewWorkspaceFileAdapter: %v", err)
	}
	return a
}

func TestWorkspaceFileAdapter_WriteRead(t *testing.T) {
	srv, _ := newMockWorkspaceServer(t, "tok123")
	a := newWorkspaceAdapter(t, srv, "tok123", "proj1")
	ctx := context.Background()

	if err := a.Write(ctx, "/Output/result.txt", []byte("hello"), workflow.WriteModeOverwrite); err != nil {
		t.Fatalf("Write: %v", err)
	}
	got, err := a.Read(ctx, "/Output/result.txt")
	if err != nil {
		t.Fatalf("Read: %v", err)
	}
	if string(got) != "hello" {
		t.Errorf("Read = %q, want %q", got, "hello")
	}
}

func TestWorkspaceFileAdapter_Exists(t *testing.T) {
	srv, _ := newMockWorkspaceServer(t, "tok123")
	a := newWorkspaceAdapter(t, srv, "tok123", "proj1")
	ctx := context.Background()

	exists, err := a.Exists(ctx, "/Output/nope.txt")
	if err != nil {
		t.Fatalf("Exists: %v", err)
	}
	if exists {
		t.Error("expected file to not exist")
	}

	_ = a.Write(ctx, "/Output/nope.txt", []byte("x"), workflow.WriteModeOverwrite)

	exists, err = a.Exists(ctx, "/Output/nope.txt")
	if err != nil {
		t.Fatalf("Exists after write: %v", err)
	}
	if !exists {
		t.Error("expected file to exist after write")
	}
}

func TestWorkspaceFileAdapter_WriteModeAppend(t *testing.T) {
	srv, _ := newMockWorkspaceServer(t, "tok123")
	a := newWorkspaceAdapter(t, srv, "tok123", "proj1")
	ctx := context.Background()

	_ = a.Write(ctx, "/log.txt", []byte("line1\n"), workflow.WriteModeOverwrite)
	_ = a.Write(ctx, "/log.txt", []byte("line2\n"), workflow.WriteModeAppend)
	got, _ := a.Read(ctx, "/log.txt")
	if string(got) != "line1\nline2\n" {
		t.Errorf("append result = %q, want %q", got, "line1\nline2\n")
	}
}

func TestWorkspaceFileAdapter_WriteModePrepend(t *testing.T) {
	srv, _ := newMockWorkspaceServer(t, "tok123")
	a := newWorkspaceAdapter(t, srv, "tok123", "proj1")
	ctx := context.Background()

	_ = a.Write(ctx, "/doc.txt", []byte("world"), workflow.WriteModeOverwrite)
	_ = a.Write(ctx, "/doc.txt", []byte("hello "), workflow.WriteModePrepend)
	got, _ := a.Read(ctx, "/doc.txt")
	if string(got) != "hello world" {
		t.Errorf("prepend result = %q, want %q", got, "hello world")
	}
}

func TestWorkspaceFileAdapter_WriteModeFailIfExists(t *testing.T) {
	srv, _ := newMockWorkspaceServer(t, "tok123")
	a := newWorkspaceAdapter(t, srv, "tok123", "proj1")
	ctx := context.Background()

	_ = a.Write(ctx, "/f.txt", []byte("v1"), workflow.WriteModeOverwrite)
	err := a.Write(ctx, "/f.txt", []byte("v2"), workflow.WriteModeFailIfExists)
	if err == nil {
		t.Fatal("expected FileExistsError, got nil")
	}
	var fee *workflow.FileExistsError
	if !errors.As(err, &fee) {
		t.Fatalf("expected FileExistsError, got %T: %v", err, err)
	}
}

func TestWorkspaceFileAdapter_List(t *testing.T) {
	srv, _ := newMockWorkspaceServer(t, "tok123")
	a := newWorkspaceAdapter(t, srv, "tok123", "proj1")
	ctx := context.Background()

	_ = a.Write(ctx, "/Output/a.txt", []byte("x"), workflow.WriteModeOverwrite)
	_ = a.Write(ctx, "/Output/b.txt", []byte("x"), workflow.WriteModeOverwrite)
	_ = a.Write(ctx, "/Other/c.md", []byte("x"), workflow.WriteModeOverwrite)

	got, err := a.List(ctx, "Output/*")
	if err != nil {
		t.Fatalf("List: %v", err)
	}
	if len(got) != 2 {
		t.Errorf("List(Output/*) = %v, want 2 items", got)
	}
	for _, p := range got {
		if !strings.HasPrefix(p, "/Output/") {
			t.Errorf("path %q should start with /Output/", p)
		}
	}
}

func TestWorkspaceFileAdapter_GID(t *testing.T) {
	srv, _ := newMockWorkspaceServer(t, "tok123")
	a := newWorkspaceAdapter(t, srv, "tok123", "my-gid")
	if a.GID() != "my-gid" {
		t.Errorf("GID = %q, want %q", a.GID(), "my-gid")
	}
}
