package api

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"

	"drivervault/apiserver/internal/config"
	"drivervault/apiserver/internal/pb"
)

// fakeSchemaPB is a PocketBase stand-in for the "app_settings does not exist"
// case: a stack upgraded with PB_BOOTSTRAP off, where the on-boot schema pass
// never created the collection the plugin settings live in.
type fakeSchemaPB struct {
	mu sync.Mutex

	created     bool // app_settings exists
	createCalls int  // POST /api/collections
	unreachable bool // every records call fails with a 502
}

func (f *fakeSchemaPB) server(t *testing.T) *httptest.Server {
	t.Helper()
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		f.mu.Lock()
		defer f.mu.Unlock()

		switch {
		case strings.HasSuffix(r.URL.Path, "/auth-with-password"):
			writeTestJSON(w, http.StatusOK, map[string]any{"token": "test-token"})

		// The settings read. 404 until the collection exists — PocketBase
		// answers a list against a missing collection that way, while an
		// existing but empty one answers 200 with no items.
		case r.Method == http.MethodGet && r.URL.Path == "/api/collections/app_settings/records":
			if f.unreachable {
				http.Error(w, "connection refused", http.StatusBadGateway)
				return
			}
			if !f.created {
				http.Error(w, `{"message":"Missing collection context."}`, http.StatusNotFound)
				return
			}
			writeTestJSON(w, http.StatusOK, map[string]any{
				"page": 1, "perPage": 1, "totalItems": 0, "totalPages": 1,
				"items": []any{},
			})

		// The schema calls EnsureCollection makes.
		case r.Method == http.MethodGet && r.URL.Path == "/api/collections":
			items := []any{
				map[string]any{"id": "col_users", "name": "users", "fields": []any{
					map[string]any{"name": "email", "type": "text"},
				}},
			}
			if f.created {
				items = append(items, map[string]any{"id": "col_app", "name": "app_settings"})
			}
			writeTestJSON(w, http.StatusOK, map[string]any{"items": items})

		case r.Method == http.MethodPost && r.URL.Path == "/api/collections":
			var body struct {
				Name string `json:"name"`
			}
			_ = json.NewDecoder(r.Body).Decode(&body)
			if body.Name != colAppSettings {
				t.Errorf("created collection %q, want %q", body.Name, colAppSettings)
			}
			f.createCalls++
			f.created = true
			writeTestJSON(w, http.StatusOK, map[string]any{"id": "col_app"})

		default:
			t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
			http.Error(w, "unexpected", http.StatusInternalServerError)
		}
	}))
	t.Cleanup(srv.Close)
	return srv
}

func writeTestJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func newSchemaTestServer(t *testing.T, f *fakeSchemaPB) *Server {
	t.Helper()
	pbSrv := f.server(t)
	return New(config.Config{UsersCollection: "users"},
		pb.New(pbSrv.URL, "admin@test.local", "pw"))
}

// The guard: a missing settings collection is created rather than retried
// forever, so a stack upgraded with the bootstrap off still comes up.
func TestLoadPluginsCreatesMissingSettingsCollection(t *testing.T) {
	f := &fakeSchemaPB{}
	s := newSchemaTestServer(t, f)

	if err := s.loadPlugins(context.Background()); err != nil {
		t.Fatalf("loadPlugins should recover by creating the collection, got %v", err)
	}
	if !s.plugins.Ready() {
		t.Fatal("plugins should be loaded after the collection was created")
	}

	f.mu.Lock()
	calls := f.createCalls
	f.mu.Unlock()
	if calls != 1 {
		t.Fatalf("expected the collection to be created once, got %d", calls)
	}
}

// Once the collection exists, nothing is created again.
func TestLoadPluginsDoesNotRecreateExistingCollection(t *testing.T) {
	f := &fakeSchemaPB{created: true}
	s := newSchemaTestServer(t, f)

	if err := s.loadPlugins(context.Background()); err != nil {
		t.Fatalf("loadPlugins: %v", err)
	}
	f.mu.Lock()
	calls := f.createCalls
	f.mu.Unlock()
	if calls != 0 {
		t.Fatalf("an existing collection must not be recreated, got %d creates", calls)
	}
}

// A database that is merely unreachable must NOT trigger schema surgery: the
// remedy there is to wait, and creating collections against a flaky database is
// exactly the wrong reflex.
func TestLoadPluginsDoesNotCreateOnOutage(t *testing.T) {
	f := &fakeSchemaPB{created: true, unreachable: true}
	s := newSchemaTestServer(t, f)

	if err := s.loadPlugins(context.Background()); err == nil {
		t.Fatal("expected an error while the database is unreachable")
	}
	if s.plugins.Ready() {
		t.Fatal("plugins must not report ready after an outage")
	}
	f.mu.Lock()
	calls := f.createCalls
	f.mu.Unlock()
	if calls != 0 {
		t.Fatalf("an outage must not create collections, got %d creates", calls)
	}
}
