package plugins

import (
	"context"
	"encoding/json"
	"errors"
	"testing"
)

// testPlugin is a no-op plugin used to exercise the manager's persistence
// without reaching for a real connector (the builtins register themselves from
// a package that imports this one, so they are not available here).
type testPlugin struct{ name string }

func (t *testPlugin) Descriptor() Descriptor                        { return Descriptor{Name: t.name} }
func (t *testPlugin) Init(context.Context, map[string]string) error { return nil }
func (t *testPlugin) Shutdown(context.Context) error                { return nil }
func (t *testPlugin) HealthCheck(context.Context) Health            { return Health{Status: "ok"} }
func (t *testPlugin) Invoke(context.Context, string, json.RawMessage) (json.RawMessage, error) {
	return nil, nil
}

func init() {
	for _, n := range []string{"test-a", "test-b"} {
		Register(n, func() Plugin { return &testPlugin{name: n} })
	}
}

// seeded returns a memory store already holding doc.
func seeded(doc string) *memoryStore {
	return &memoryStore{data: []byte(doc), found: true}
}

// loadedManager returns a Manager that has successfully read doc.
func loadedManager(t *testing.T, store Store) *Manager {
	t.Helper()
	m := NewManager(store)
	if err := m.Load(context.Background()); err != nil {
		t.Fatalf("Load: %v", err)
	}
	if !m.Ready() {
		t.Fatal("manager should be ready after a successful load")
	}
	return m
}

// A document holding "null" (or nothing) used to decode to a nil map, which
// made the next save panic with "assignment to entry in nil map".
func TestLoadNullDocumentDoesNotPanic(t *testing.T) {
	for _, doc := range []string{"null", "", "  \n", "{}"} {
		m := loadedManager(t, seeded(doc))
		if _, err := m.Upsert(context.Background(), "test-a", true, nil); err != nil {
			t.Fatalf("Upsert after %q document: %v", doc, err)
		}
		if v, _ := m.Get("test-a"); !v.Enabled {
			t.Fatalf("plugin not enabled after save (document was %q)", doc)
		}
	}
}

// A null entry for a single plugin left a nil *record that Load dereferenced.
func TestLoadNullRecordIsDropped(t *testing.T) {
	m := loadedManager(t, seeded(`{"test-a":null,"test-b":{"enabled":true}}`))
	if v, _ := m.Get("test-b"); !v.Enabled {
		t.Fatal("test-b should still be enabled")
	}
	if v, _ := m.Get("test-a"); v.Enabled {
		t.Fatal("test-a should not be enabled")
	}
}

// An unreachable store must leave the Manager unloaded and refusing to write,
// so an outage cannot overwrite settings that were never read.
func TestUnreachableStoreRefusesEveryMutation(t *testing.T) {
	store := &memoryStore{failLoad: errNotReady}
	m := NewManager(store)

	if err := m.Load(context.Background()); !IsNotReady(err) {
		t.Fatalf("expected a not-ready error, got %v", err)
	}
	if m.Ready() {
		t.Fatal("manager must not report ready after a failed load")
	}

	ctx := context.Background()
	if _, err := m.Upsert(ctx, "test-a", true, nil); !IsNotReady(err) {
		t.Fatalf("Upsert should refuse while unloaded, got %v", err)
	}
	if err := m.RegisterExternal(ctx, "ext", "http://example.test", ""); !IsNotReady(err) {
		t.Fatalf("RegisterExternal should refuse while unloaded, got %v", err)
	}
	if err := m.Remove(ctx, "test-a"); !IsNotReady(err) {
		t.Fatalf("Remove should refuse while unloaded, got %v", err)
	}
	if store.data != nil {
		t.Fatal("nothing may be written to a store that was never read")
	}
}

// A document that does not parse must not be replaced by an empty one — the
// read problem must stay a read problem rather than becoming data loss.
func TestUnreadableDocumentIsNeverOverwritten(t *testing.T) {
	original := `{"test-a":{"enabled":true,"config":{"user":"me"}}}garbage`
	store := seeded(original)
	m := NewManager(store)

	err := m.Load(context.Background())
	if err == nil {
		t.Fatal("Load should report an unreadable document")
	}
	if m.Ready() {
		t.Fatal("manager must not report ready after an unreadable document")
	}
	if _, err := m.Upsert(context.Background(), "test-a", true, nil); !IsNotReady(err) {
		t.Fatalf("Upsert should refuse, got %v", err)
	}
	if string(store.data) != original {
		t.Fatalf("the unreadable document was modified: %s", store.data)
	}
}

// A failed save must roll back, so callers never see a change that vanishes.
func TestUpsertRollsBackWhenSaveFails(t *testing.T) {
	store := seeded(`{"test-a":{"enabled":true,"config":{"user":"first"}}}`)
	m := loadedManager(t, store)

	store.failSave = errors.New("database is gone")

	_, err := m.Upsert(context.Background(), "test-a", false, map[string]string{"user": "second"})
	if err == nil {
		t.Fatal("expected an error when the store cannot be written")
	}
	if !IsPersist(err) {
		t.Fatalf("error should be classified as a persist failure, got %v", err)
	}
	v, _ := m.Get("test-a")
	if !v.Enabled || v.Config["user"] != "first" {
		t.Fatalf("failed save leaked into memory: enabled=%v user=%q", v.Enabled, v.Config["user"])
	}
}

// A newly created record must not survive a save that failed.
func TestUpsertRollsBackNewRecord(t *testing.T) {
	store := seeded(`{}`)
	m := loadedManager(t, store)
	store.failSave = errors.New("database is gone")

	if _, err := m.Upsert(context.Background(), "test-a", true, nil); !IsPersist(err) {
		t.Fatalf("expected persist failure, got %v", err)
	}
	if v, _ := m.Get("test-a"); v.Enabled {
		t.Fatal("a record from a failed save is still present")
	}
}

// Enable/disable state must survive a restart — the whole point of the store.
func TestStateSurvivesReload(t *testing.T) {
	store := seeded(`{}`)
	m := loadedManager(t, store)
	if _, err := m.Upsert(context.Background(), "test-a", true, map[string]string{"user": "me"}); err != nil {
		t.Fatal(err)
	}

	// Restart against the same store.
	m2 := loadedManager(t, store)
	v, ok := m2.Get("test-a")
	if !ok || !v.Enabled || v.Config["user"] != "me" {
		t.Fatalf("state lost across restart: ok=%v enabled=%v config=%v", ok, v.Enabled, v.Config)
	}
}

// External plugins are registered and removed through the same store.
func TestExternalPluginRoundTrip(t *testing.T) {
	store := seeded(`{}`)
	m := loadedManager(t, store)
	ctx := context.Background()

	if err := m.RegisterExternal(ctx, "ext", "example.test/api", "ACME"); err != nil {
		t.Fatalf("RegisterExternal: %v", err)
	}
	m2 := loadedManager(t, store)
	if v, ok := m2.Get("ext"); !ok || v.BaseURL != "http://example.test/api" {
		t.Fatalf("external plugin did not persist: ok=%v baseURL=%q", ok, v.BaseURL)
	}

	if err := m2.Remove(ctx, "ext"); err != nil {
		t.Fatalf("Remove: %v", err)
	}
	m3 := loadedManager(t, store)
	if _, ok := m3.Get("ext"); ok {
		t.Fatal("removed external plugin came back")
	}
}
