package plugins

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"testing"
)

// ---- ParseSource ----

func TestParseSource_BareNameBecomesLocal(t *testing.T) {
	s, err := ParseSource("my-plugin")
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if s.Scheme != "local" || s.Spec != "my-plugin" {
		t.Errorf("got %+v", s)
	}
}

func TestParseSource_ExplicitScheme(t *testing.T) {
	cases := map[string]Source{
		"local://foo":            {Scheme: "local", Spec: "foo"},
		"github://org/repo":      {Scheme: "github", Spec: "org/repo"},
		"github://org/repo#v1.0": {Scheme: "github", Spec: "org/repo#v1.0"},
		"clawhub://name@1.2.3":   {Scheme: "clawhub", Spec: "name@1.2.3"},
		"https://example.com/x":  {Scheme: "https", Spec: "example.com/x"},
	}
	for in, want := range cases {
		t.Run(in, func(t *testing.T) {
			got, err := ParseSource(in)
			if err != nil {
				t.Fatalf("unexpected err: %v", err)
			}
			if !reflect.DeepEqual(got, want) {
				t.Errorf("ParseSource(%q) = %+v, want %+v", in, got, want)
			}
		})
	}
}

func TestParseSource_EmptyRejected(t *testing.T) {
	if _, err := ParseSource(""); err == nil {
		t.Error("expected error on empty input")
	}
	if _, err := ParseSource("   "); err == nil {
		t.Error("expected error on whitespace input")
	}
}

func TestParseSource_StripsWhitespace(t *testing.T) {
	s, err := ParseSource("  my-plugin  ")
	if err != nil || s.Spec != "my-plugin" {
		t.Errorf("got %+v, err=%v", s, err)
	}
}

func TestSource_Raw(t *testing.T) {
	s := Source{Scheme: "github", Spec: "foo/bar#v1"}
	if s.Raw() != "github://foo/bar#v1" {
		t.Errorf("got %q", s.Raw())
	}
}

// ---- Registry ----

type fakeResolver struct {
	scheme string
	calls  int
}

func (f *fakeResolver) Scheme() string { return f.scheme }
func (f *fakeResolver) Fetch(ctx context.Context, spec, dst string) (string, error) {
	f.calls++
	return spec, nil
}

func TestRegistry_RegisterAndResolve(t *testing.T) {
	reg := NewRegistry()
	local := &fakeResolver{scheme: "local"}
	gh := &fakeResolver{scheme: "github"}
	reg.Register(local)
	reg.Register(gh)

	r, err := reg.Resolve(Source{Scheme: "github", Spec: "x/y"})
	if err != nil {
		t.Fatal(err)
	}
	if r != gh {
		t.Errorf("got wrong resolver: %+v", r)
	}
}

func TestRegistry_UnknownScheme(t *testing.T) {
	reg := NewRegistry()
	_, err := reg.Resolve(Source{Scheme: "mystery", Spec: "x"})
	if err == nil {
		t.Error("expected error for unknown scheme")
	}
	if !strings.Contains(err.Error(), "mystery") {
		t.Errorf("error should name the missing scheme: %v", err)
	}
}

func TestRegistry_OverwriteSameScheme(t *testing.T) {
	reg := NewRegistry()
	a := &fakeResolver{scheme: "local"}
	b := &fakeResolver{scheme: "local"}
	reg.Register(a)
	reg.Register(b)
	r, _ := reg.Resolve(Source{Scheme: "local", Spec: "x"})
	if r != b {
		t.Error("second registration should overwrite the first")
	}
}

func TestRegistry_SchemesSorted(t *testing.T) {
	reg := NewRegistry()
	reg.Register(&fakeResolver{scheme: "local"})
	reg.Register(&fakeResolver{scheme: "clawhub"})
	reg.Register(&fakeResolver{scheme: "github"})
	got := reg.Schemes()
	want := []string{"clawhub", "github", "local"}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("Schemes() = %v, want %v", got, want)
	}
}

func TestRegistry_EmptyReturnsEmpty(t *testing.T) {
	reg := NewRegistry()
	if s := reg.Schemes(); len(s) != 0 {
		t.Errorf("empty registry should return empty slice, got %v", s)
	}
}

func TestErrPluginNotFound_IsMatchable(t *testing.T) {
	// Wrap + unwrap via fmt.Errorf to prove errors.Is works through
	// the fmt wrappers the resolvers use in their error returns.
	err := fmt.Errorf("local resolver: plugin \"x\": %w", ErrPluginNotFound)
	if !errors.Is(err, ErrPluginNotFound) {
		t.Error("errors.Is did not unwrap ErrPluginNotFound")
	}
}

func TestSource_StringEqualsRaw(t *testing.T) {
	s := Source{Scheme: "github", Spec: "foo/bar#v1"}
	if s.String() != s.Raw() {
		t.Errorf("String()=%q Raw()=%q must match", s.String(), s.Raw())
	}
}

func TestRegistry_ConcurrentRegisterResolve_NoRace(t *testing.T) {
	// Exercises the RWMutex: interleave Register / Resolve / Schemes
	// from multiple goroutines. `go test -race` fails loudly if the
	// locking is wrong.
	reg := NewRegistry()
	reg.Register(&fakeResolver{scheme: "local"})

	done := make(chan struct{})
	for i := 0; i < 4; i++ {
		go func(i int) {
			for j := 0; j < 50; j++ {
				reg.Register(&fakeResolver{scheme: fmt.Sprintf("s%d", i)})
				_, _ = reg.Resolve(Source{Scheme: "local"})
				_ = reg.Schemes()
			}
			done <- struct{}{}
		}(i)
	}
	for i := 0; i < 4; i++ {
		<-done
	}
}

// ---- C1: empty spec after scheme ----

func TestParseSource_EmptySpecAfterSchemeRejected(t *testing.T) {
	for _, in := range []string{"local://", "github://", "https://", "local://   "} {
		t.Run(in, func(t *testing.T) {
			_, err := ParseSource(in)
			if err == nil {
				t.Errorf("ParseSource(%q) should reject empty spec", in)
			} else if !strings.Contains(err.Error(), "empty spec") {
				t.Errorf("error message should mention 'empty spec': %v", err)
			}
		})
	}
}

func TestParseSource_BareNameStillAccepted(t *testing.T) {
	// The empty-spec guard must not break back-compat for bare names.
	s, err := ParseSource("my-plugin")
	if err != nil || s.Scheme != "local" || s.Spec != "my-plugin" {
		t.Errorf("bare name broke: %+v err=%v", s, err)
	}
}
