package agenttool

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"github.com/smallnest/pigo/internal/agentcore"
)

// newTestBlackboard builds a BlackboardTool over a fresh temp dir and a helper
// to run an action and return the text result.
func newTestBlackboard(t *testing.T) (*BlackboardTool, func(args string) string) {
	t.Helper()
	root := t.TempDir()
	tool := &BlackboardTool{Root: root}
	run := func(args string) string {
		t.Helper()
		res, err := tool.Execute(context.Background(), "t1", json.RawMessage(args), nil)
		if err != nil {
			t.Fatalf("Execute: %v", err)
		}
		var sb strings.Builder
		for _, c := range res.Content {
			if txt, ok := c.(agentcore.TextContent); ok {
				sb.WriteString(txt.Text)
			}
		}
		return sb.String()
	}
	return tool, run
}

func TestBlackboardPostAndRead(t *testing.T) {
	tool, run := newTestBlackboard(t)

	got := run(`{"action":"post","file":"round-1-a.md","content":"hello from a"}`)
	if !strings.Contains(got, "appended") {
		t.Fatalf("post result = %q, want appended confirmation", got)
	}

	data, err := os.ReadFile(filepath.Join(tool.Root, "messages", "round-1-a.md"))
	if err != nil {
		t.Fatalf("message file not written: %v", err)
	}
	if string(data) != "hello from a" {
		t.Fatalf("message content = %q, want %q", data, "hello from a")
	}

	// The snapshot lists the message; readFile returns its contents.
	snap := run(`{"action":"read"}`)
	if !strings.Contains(snap, "round-1-a.md") {
		t.Fatalf("snapshot missing the message name:\n%s", snap)
	}
	if !strings.Contains(snap, "DONE") || !strings.Contains(snap, "not created") {
		t.Fatalf("snapshot missing DONE status:\n%s", snap)
	}
	one := run(`{"action":"read","path":"messages/round-1-a.md"}`)
	if !strings.Contains(one, "hello from a") {
		t.Fatalf("readFile result missing content:\n%s", one)
	}
}

func TestBlackboardPostRejectsTraversal(t *testing.T) {
	tool, run := newTestBlackboard(t)
	for _, bad := range []string{
		`{"action":"post","file":"../escape.md","content":"x"}`,
		`{"action":"post","file":"a/b.md","content":"x"}`,
		`{"action":"post","file":"..","content":"x"}`,
		`{"action":"post","file":"notes.txt","content":"x"}`,
		`{"action":"post","file":"round.md","content":""}`,
	} {
		got := run(bad)
		if strings.Contains(got, "appended") {
			t.Fatalf("post with %s must be rejected, got %q", bad, got)
		}
	}
	if _, err := os.Stat(filepath.Join(tool.Root, "escape.md")); !os.IsNotExist(err) {
		t.Fatalf("traversal escaped the root: %v", err)
	}
}

func TestBlackboardReadRejectsTraversal(t *testing.T) {
	_, run := newTestBlackboard(t)
	for _, bad := range []string{
		`{"action":"read","path":"../outside.md"}`,
		`{"action":"read","path":"/etc/passwd"}`,
		`{"action":"read","path":"messages"}`,
	} {
		got := run(bad)
		if !strings.Contains(got, "blackboard read:") {
			t.Fatalf("read with %s must error, got %q", bad, got)
		}
	}
}

func TestBlackboardDoneIsExclusive(t *testing.T) {
	tool, run := newTestBlackboard(t)

	got := run(`{"action":"done","summary":"delivered: flag=abc"}`)
	if !strings.Contains(got, "DONE marker created") {
		t.Fatalf("first done failed: %q", got)
	}
	data, err := os.ReadFile(filepath.Join(tool.Root, "DONE"))
	if err != nil {
		t.Fatalf("DONE not written: %v", err)
	}
	if !strings.Contains(string(data), "flag=abc") {
		t.Fatalf("DONE content missing summary: %q", data)
	}

	// A second done must report the existing marker, not overwrite it.
	got2 := run(`{"action":"done","summary":"another summary"}`)
	if !strings.Contains(got2, "already exists") {
		t.Fatalf("second done must report existing marker, got %q", got2)
	}
	data2, _ := os.ReadFile(filepath.Join(tool.Root, "DONE"))
	if strings.Contains(string(data2), "another summary") {
		t.Fatalf("second done overwrote the marker: %q", data2)
	}

	// done with an empty summary is rejected.
	if got := run(`{"action":"done","summary":""}`); !strings.Contains(got, "summary must not be empty") {
		t.Fatalf("empty-summary done must error, got %q", got)
	}
}

// TestBlackboardPostConcurrentAtomic verifies that parallel posts to the same
// message file never interleave or lose bytes: each message survives whole.
func TestBlackboardPostConcurrentAtomic(t *testing.T) {
	tool, _ := newTestBlackboard(t)
	const n = 16
	msgs := make([]string, n)
	for i := range msgs {
		msgs[i] = strings.Repeat("M", 100) + string(rune('A'+i)) + strings.Repeat("N", 100)
	}

	var wg sync.WaitGroup
	for i := 0; i < n; i++ {
		i := i
		wg.Add(1)
		go func() {
			defer wg.Done()
			args := json.RawMessage(`{"action":"post","file":"round-1-a.md","content":"` + msgs[i] + `"}`)
			if _, err := tool.Execute(context.Background(), "t", args, nil); err != nil {
				t.Errorf("concurrent post %d: %v", i, err)
			}
		}()
	}
	wg.Wait()

	data, err := os.ReadFile(filepath.Join(tool.Root, "messages", "round-1-a.md"))
	if err != nil {
		t.Fatalf("read messages: %v", err)
	}
	got := string(data)
	for i, m := range msgs {
		if !strings.Contains(got, m) {
			t.Fatalf("message %d lost/interleaved in concurrent append:\n%s", i, got)
		}
	}
	// Every message must appear exactly once (no duplication from re-read+write).
	for _, m := range msgs {
		if strings.Count(got, m) != 1 {
			t.Fatalf("message %q appears %d times:\n%s", m, strings.Count(got, m), got)
		}
	}
}

func TestBlackboardPostSizeCap(t *testing.T) {
	_, run := newTestBlackboard(t)
	huge := strings.Repeat("x", maxBlackboardMessageBytes+1)
	got := run(`{"action":"post","file":"big.md","content":"` + huge + `"}`)
	if !strings.Contains(got, "too large") {
		t.Fatalf("oversized post must error, got %q", got)
	}
}

func TestBlackboardUnknownAction(t *testing.T) {
	_, run := newTestBlackboard(t)
	if got := run(`{"action":"bogus"}`); !strings.Contains(got, "unknown action") {
		t.Fatalf("unknown action must error, got %q", got)
	}
}

func TestBlackboardNoRoot(t *testing.T) {
	tool := &BlackboardTool{}
	res, err := tool.Execute(context.Background(), "t", json.RawMessage(`{"action":"read"}`), nil)
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	txt := res.Content[0].(agentcore.TextContent).Text
	if !strings.Contains(txt, "no blackboard root") {
		t.Fatalf("no-root read must error, got %q", txt)
	}
}
