package platform

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

// collect records everything the monitor decides to upload.
func newCollector() (*[]string, Sink) {
	var got []string
	return &got, func(text string) { got = append(got, text) }
}

func TestMonitor_UploadsNewText(t *testing.T) {
	got, sink := newCollector()
	m := NewMonitor(sink)

	if !m.Observe(ClipboardEvent{Text: "hello"}) {
		t.Error("first new text should be accepted")
	}
	if len(*got) != 1 || (*got)[0] != "hello" {
		t.Errorf("uploads = %v, want [hello]", *got)
	}
}

func TestMonitor_DropsSensitive(t *testing.T) {
	got, sink := newCollector()
	m := NewMonitor(sink)

	if m.Observe(ClipboardEvent{Text: "hunter2", Sensitive: true}) {
		t.Error("sensitive content must not be accepted")
	}
	if len(*got) != 0 {
		t.Errorf("uploads = %v, want none", *got)
	}
}

func TestMonitor_DropsEmpty(t *testing.T) {
	got, sink := newCollector()
	m := NewMonitor(sink)
	if m.Observe(ClipboardEvent{Text: ""}) {
		t.Error("empty text must not be accepted")
	}
	if len(*got) != 0 {
		t.Errorf("uploads = %v, want none", *got)
	}
}

func TestMonitor_DedupsRepeats(t *testing.T) {
	got, sink := newCollector()
	m := NewMonitor(sink)

	m.Observe(ClipboardEvent{Text: "same"})
	if m.Observe(ClipboardEvent{Text: "same"}) {
		t.Error("identical repeat should be dropped")
	}
	if len(*got) != 1 {
		t.Errorf("uploads = %v, want one", *got)
	}
	// a different value re-arms uploads
	if !m.Observe(ClipboardEvent{Text: "other"}) {
		t.Error("changed text should be accepted")
	}
}

func TestMonitor_LoopGuardSuppressesOwnEcho(t *testing.T) {
	got, sink := newCollector()
	m := NewMonitor(sink)

	// We write "from-cloud" locally, then the OS reports that same change.
	m.WillWrite("from-cloud")
	if m.Observe(ClipboardEvent{Text: "from-cloud"}) {
		t.Error("our own write echo must not be re-uploaded")
	}
	if len(*got) != 0 {
		t.Errorf("uploads = %v, want none (echo suppressed)", *got)
	}

	// A genuinely new copy afterwards still uploads.
	if !m.Observe(ClipboardEvent{Text: "typed-by-user"}) {
		t.Error("subsequent real change should be accepted")
	}
	if len(*got) != 1 || (*got)[0] != "typed-by-user" {
		t.Errorf("uploads = %v, want [typed-by-user]", *got)
	}
}

// fakeSource feeds a fixed list of events into Watch then closes.
type fakeSource struct {
	events []ClipboardEvent
}

func (f *fakeSource) Watch(ctx context.Context) <-chan ClipboardEvent {
	ch := make(chan ClipboardEvent)
	go func() {
		defer close(ch)
		for _, ev := range f.events {
			select {
			case <-ctx.Done():
				return
			case ch <- ev:
			}
		}
	}()
	return ch
}

func (f *fakeSource) Write(string) error { return nil }

func TestRun_DrivesSourceThroughMonitor(t *testing.T) {
	got, sink := newCollector()
	m := NewMonitor(sink)
	src := &fakeSource{events: []ClipboardEvent{
		{Text: "a"},
		{Text: "secret", Sensitive: true},
		{Text: "a"}, // dedup
		{Text: "b"},
	}}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	Run(ctx, src, m)

	want := []string{"a", "b"}
	if len(*got) != len(want) || (*got)[0] != "a" || (*got)[1] != "b" {
		t.Errorf("uploads = %v, want %v", *got, want)
	}
}

// recordingSource records Write calls; Watch is unused for the download tests.
type recordingSource struct {
	writes []string
	err    error
}

func (r *recordingSource) Watch(context.Context) <-chan ClipboardEvent {
	ch := make(chan ClipboardEvent)
	close(ch)
	return ch
}

func (r *recordingSource) Write(text string) error {
	if r.err != nil {
		return r.err
	}
	r.writes = append(r.writes, text)
	return nil
}

func TestApplyRemote_WritesNewContent(t *testing.T) {
	src := &recordingSource{}
	m := NewMonitor(func(string) {})
	if !m.ApplyRemote(src, ClipboardContent{Content: "hi", SourceDevice: "other"}, "me") {
		t.Error("new remote content should be written locally")
	}
	if len(src.writes) != 1 || src.writes[0] != "hi" {
		t.Errorf("writes = %v, want [hi]", src.writes)
	}
}

func TestApplyRemote_SkipsSelfSource(t *testing.T) {
	src := &recordingSource{}
	m := NewMonitor(func(string) {})
	if m.ApplyRemote(src, ClipboardContent{Content: "hi", SourceDevice: "me"}, "me") {
		t.Error("our own echoed upload must be skipped")
	}
	if len(src.writes) != 0 {
		t.Errorf("writes = %v, want none", src.writes)
	}
}

func TestApplyRemote_SkipsEmptyAndCurrent(t *testing.T) {
	src := &recordingSource{}
	m := NewMonitor(func(string) {})
	if m.ApplyRemote(src, ClipboardContent{Content: "", SourceDevice: "other"}, "me") {
		t.Error("empty remote content must be skipped")
	}
	m.Observe(ClipboardEvent{Text: "X"}) // lastHash now = hash(X)
	if m.ApplyRemote(src, ClipboardContent{Content: "X", SourceDevice: "other"}, "me") {
		t.Error("content equal to current must be skipped")
	}
}

// The full loop guard across both directions: apply a remote change, then the
// Source's resulting local change event must not be re-uploaded.
func TestApplyRemote_LoopGuardAcrossObserve(t *testing.T) {
	src := &recordingSource{}
	var uploads []string
	m := NewMonitor(func(text string) { uploads = append(uploads, text) })

	m.ApplyRemote(src, ClipboardContent{Content: "cloud", SourceDevice: "other"}, "me")
	if m.Observe(ClipboardEvent{Text: "cloud"}) {
		t.Error("echo of an applied-remote write must be suppressed")
	}
	if len(uploads) != 0 {
		t.Errorf("uploads = %v, want none", uploads)
	}
}
