package gitoevents

import (
	"context"
	"errors"
	"io"
	"sync"
	"testing"

	"github.com/nomadcode/nomadcode-core/internal/protosocket"
)

// fakeTransport is a scripted Transport for driving Client in tests.
type fakeTransport struct {
	mu        sync.Mutex
	connected bool
	closed    bool
	written   []protosocket.Envelope
	inbound   []protosocket.Envelope
	readIdx   int
}

func (f *fakeTransport) Connect(context.Context) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.connected = true
	return nil
}

func (f *fakeTransport) Write(_ context.Context, env protosocket.Envelope) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.written = append(f.written, env)
	return nil
}

func (f *fakeTransport) Read(context.Context) (protosocket.Envelope, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.readIdx >= len(f.inbound) {
		return protosocket.Envelope{}, io.EOF
	}
	env := f.inbound[f.readIdx]
	f.readIdx++
	return env, nil
}

func (f *fakeTransport) Close() error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.closed = true
	return nil
}

func TestClientSendsSubscribeEnvelope(t *testing.T) {
	ft := &fakeTransport{}
	client, err := NewClient(ft, Options{RepoID: "nomadcode", Branch: "develop"})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	if err := client.Run(context.Background()); !errors.Is(err, io.EOF) {
		t.Fatalf("Run: got %v, want io.EOF", err)
	}

	if !ft.connected {
		t.Fatal("expected transport to be connected")
	}
	if !ft.closed {
		t.Fatal("expected transport to be closed")
	}
	if len(ft.written) != 1 {
		t.Fatalf("written envelopes: got %d, want 1", len(ft.written))
	}
	sub := ft.written[0]
	if sub.Channel != EventChannel || sub.Action != SubscribeAction {
		t.Fatalf("subscribe envelope: got %q/%q", sub.Channel, sub.Action)
	}
	if sub.Payload["repo_id"] != "nomadcode" || sub.Payload["branch"] != "develop" {
		t.Fatalf("subscribe payload: got %v", sub.Payload)
	}
}

func TestClientForwardsTargetEventToHandler(t *testing.T) {
	ft := &fakeTransport{
		inbound: []protosocket.Envelope{branchUpdatedEnvelope()},
	}

	var got []BranchUpdatedEvent
	client, err := NewClient(ft, Options{
		RepoID: "nomadcode",
		Branch: "develop",
		Handler: func(_ context.Context, ev BranchUpdatedEvent) {
			got = append(got, ev)
		},
	})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	if err := client.Run(context.Background()); !errors.Is(err, io.EOF) {
		t.Fatalf("Run: got %v, want io.EOF", err)
	}

	if len(got) != 1 {
		t.Fatalf("handler events: got %d, want 1", len(got))
	}
	if got[0].RepoID != "nomadcode" || got[0].After != "new-sha" {
		t.Fatalf("forwarded event: got %+v", got[0])
	}
}

func TestClientDropsOffTargetAndMalformedEvents(t *testing.T) {
	offRepo := branchUpdatedEnvelope()
	offRepo.Payload["repo_id"] = "other-repo"

	offBranch := branchUpdatedEnvelope()
	offBranch.Payload["branch"] = "main"

	wrongAction := branchUpdatedEnvelope()
	wrongAction.Action = "task.status.changed"

	malformed := branchUpdatedEnvelope()
	malformed.Payload["changed_files"] = "not-a-list"

	noMilestone := branchUpdatedEnvelope()
	noMilestone.Payload["changed_files"] = []any{
		map[string]any{
			"path":        "README.md",
			"change_type": "modified",
		},
	}

	onTarget := branchUpdatedEnvelope()

	ft := &fakeTransport{
		inbound: []protosocket.Envelope{offRepo, offBranch, wrongAction, malformed, noMilestone, onTarget},
	}

	var got []BranchUpdatedEvent
	client, err := NewClient(ft, Options{
		RepoID: "nomadcode",
		Branch: "develop",
		Handler: func(_ context.Context, ev BranchUpdatedEvent) {
			got = append(got, ev)
		},
	})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	if err := client.Run(context.Background()); !errors.Is(err, io.EOF) {
		t.Fatalf("Run: got %v, want io.EOF", err)
	}

	if len(got) != 1 {
		t.Fatalf("handler events: got %d, want only the on-target event", len(got))
	}
}

func TestClientStopsOnContextCancel(t *testing.T) {
	ft := &fakeTransport{
		inbound: []protosocket.Envelope{branchUpdatedEnvelope()},
	}

	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	client, err := NewClient(ft, Options{RepoID: "nomadcode", Branch: "develop"})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	if err := client.Run(ctx); err != nil {
		t.Fatalf("Run with cancelled context: got %v, want nil", err)
	}
}

func TestNewClientRequiresRepoID(t *testing.T) {
	if _, err := NewClient(&fakeTransport{}, Options{}); err == nil {
		t.Fatal("expected error when repo_id is empty")
	}
}

func TestNewClientRequiresTransport(t *testing.T) {
	if _, err := NewClient(nil, Options{RepoID: "nomadcode"}); err == nil {
		t.Fatal("expected error when transport is nil")
	}
}
