package tools

import (
	"context"
	"strings"
	"testing"
	"time"
)

func TestRunCommandWithStdin_PassesPayload(t *testing.T) {
	// `cat` echoes stdin to stdout — perfect lightweight subject.
	if !IsCommandAvailable("cat") {
		t.Skip("cat not in PATH")
	}
	out, err := RunCommandWithStdin(context.Background(), "hello world\n", "cat")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out != "hello world\n" {
		t.Errorf("stdin payload not propagated; got %q", out)
	}
}

func TestRunCommandWithStdin_EmptyStdinIsLegacyBehavior(t *testing.T) {
	// When stdin is empty the function must not block waiting for
	// input — same behavior as the old RunCommand. `printf hi` is
	// stdin-free so any block would surface as a context timeout.
	if !IsCommandAvailable("printf") {
		t.Skip("printf not in PATH")
	}
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	out, err := RunCommandWithStdin(ctx, "", "printf", "hi")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out != "hi" {
		t.Errorf("got %q want %q", out, "hi")
	}
}

func TestRunCommandWithStdin_ContextCancelKillsChild(t *testing.T) {
	// `cat` with empty stdin will block on EOF; CommandContext must
	// terminate it when the context is canceled.
	if !IsCommandAvailable("cat") {
		t.Skip("cat not in PATH")
	}
	ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
	defer cancel()
	// Use `sleep` so the process doesn't return on its own.
	if !IsCommandAvailable("sleep") {
		t.Skip("sleep not in PATH")
	}
	_, err := RunCommandWithStdin(ctx, "", "sleep", "5")
	if err == nil {
		t.Fatal("expected error when context expires during command run")
	}
}

func TestRunToolCommandWithStdin_PopulatesResultFields(t *testing.T) {
	if !IsCommandAvailable("printf") {
		t.Skip("printf not in PATH")
	}
	res := RunToolCommandWithStdin(context.Background(),
		"printf-tool", "target.example", "",
		2*time.Second, "printf", "%s", "ok")
	if res.ToolName != "printf-tool" {
		t.Errorf("ToolName = %q want printf-tool", res.ToolName)
	}
	if res.Target != "target.example" {
		t.Errorf("Target = %q want target.example", res.Target)
	}
	if res.RawOutput != "ok" {
		t.Errorf("RawOutput = %q want ok", res.RawOutput)
	}
	if res.Duration <= 0 {
		t.Errorf("Duration not populated: %v", res.Duration)
	}
	if res.Error != nil {
		t.Errorf("unexpected Error: %v", res.Error)
	}
}

func TestRunToolCommand_DelegatesToWithStdin(t *testing.T) {
	// The non-stdin variant must remain functionally equivalent.
	if !IsCommandAvailable("printf") {
		t.Skip("printf not in PATH")
	}
	res := RunToolCommand(context.Background(),
		"t", "tgt", 2*time.Second, "printf", "%s", "delegated")
	if res.RawOutput != "delegated" {
		t.Errorf("RunToolCommand delegate broken; got %q", res.RawOutput)
	}
}

func TestParseJSONLines_DecodesWellFormedObjects(t *testing.T) {
	input := `{"host":"a","port":80}
{"host":"b","port":443}
`
	got := parseJSONLines(input)
	if len(got) != 2 {
		t.Fatalf("expected 2 records, got %d: %v", len(got), got)
	}
	if got[0]["host"] != "a" {
		t.Errorf("first record host = %v want a", got[0]["host"])
	}
	// port comes back as float64 from encoding/json.
	if got[1]["port"] != float64(443) {
		t.Errorf("second record port = %v (%T) want 443", got[1]["port"], got[1]["port"])
	}
}

func TestParseJSONLines_RawFallbackForMalformed(t *testing.T) {
	// First line is valid JSON, second is broken (missing closing
	// brace). Backward-compat: malformed lines must NOT abort
	// parsing — they ship as {"raw": <line>}.
	input := `{"ok":1}
{this is not json
{"after":"broken"}
`
	got := parseJSONLines(input)
	if len(got) != 3 {
		t.Fatalf("expected 3 records (well-formed + raw fallback + well-formed); got %d", len(got))
	}
	if got[0]["ok"] != float64(1) {
		t.Errorf("first record decoded wrong: %v", got[0])
	}
	raw, ok := got[1]["raw"].(string)
	if !ok || !strings.Contains(raw, "this is not json") {
		t.Errorf("malformed line not preserved as raw fallback: %v", got[1])
	}
	if got[2]["after"] != "broken" {
		t.Errorf("parser must keep going after malformed line; got %v", got[2])
	}
}

func TestParseJSONLines_IgnoresBlankAndNonObjectLines(t *testing.T) {
	input := "\n   \n[1,2,3]\n{\"x\":1}\nplain text\n"
	got := parseJSONLines(input)
	// Only the {"x":1} line starts with '{' and parses cleanly. The
	// `[1,2,3]` array line and `plain text` are dropped by the
	// '{'-prefix guard.
	if len(got) != 1 {
		t.Fatalf("expected 1 record, got %d: %v", len(got), got)
	}
	if got[0]["x"] != float64(1) {
		t.Errorf("expected x=1, got %v", got[0])
	}
}
