package api

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"

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

// These tests cover the self-service organization flows through the real
// Handler + middleware chain: an org-less user creating an org and being
// promoted to its admin (including the rollback when that promotion fails), and
// an admin renaming or deleting only their own organization. A stand-in
// PocketBase (orgFakePB) serves exactly the endpoints those paths hit.

const (
	orgTestUserID = "u1"
	orgTestBearer = "user-bearer-token"
	orgTestNewID  = "neworg1"
)

// orgFakePB answers the identity, organization and user-record calls the org
// handlers make, and records the writes so tests can assert on them.
type orgFakePB struct {
	mu sync.Mutex

	// Identity returned by auth-refresh.
	callerRole string
	callerOrg  string

	// Behaviour switches.
	userPatchStatus int // status for a caller promotion/demotion (200 when 0)
	memberCount     int // totalItems for the blocking-member query

	// Recorded effects.
	createdOrgs []string
	orgPatches  []map[string]any
	orgDeletes  []string
	userPatches []map[string]any
}

func (f *orgFakePB) handler(t *testing.T) http.Handler {
	t.Helper()
	mux := http.NewServeMux()

	// Service-account auth (pb.Client.Authenticate).
	mux.HandleFunc("POST /api/collections/_superusers/auth-with-password", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(w, 200, map[string]any{"token": "svc-token"})
	})

	// Identify the bearer (withAuth → identify → AuthRefresh).
	mux.HandleFunc("POST /api/collections/users/auth-refresh", func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Authorization") == "" {
			writeJSON(w, 401, map[string]any{})
			return
		}
		f.mu.Lock()
		defer f.mu.Unlock()
		writeJSON(w, 200, map[string]any{"record": map[string]any{
			"id": orgTestUserID, "email": "owner@test.local", "name": "Owner",
			"role": f.callerRole, "organization": f.callerOrg,
		}})
	})

	// Organization create / rename / delete.
	mux.HandleFunc("POST /api/collections/organizations/records", func(w http.ResponseWriter, r *http.Request) {
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		f.mu.Lock()
		f.createdOrgs = append(f.createdOrgs, orgTestNewID)
		f.mu.Unlock()
		writeJSON(w, 200, map[string]any{"id": orgTestNewID, "name": body["name"], "created": "2026-08-17 10:00:00Z"})
	})
	mux.HandleFunc("PATCH /api/collections/organizations/records/{id}", func(w http.ResponseWriter, r *http.Request) {
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		f.mu.Lock()
		f.orgPatches = append(f.orgPatches, body)
		f.mu.Unlock()
		writeJSON(w, 200, map[string]any{"id": r.PathValue("id"), "name": body["name"]})
	})
	mux.HandleFunc("DELETE /api/collections/organizations/records/{id}", func(w http.ResponseWriter, r *http.Request) {
		f.mu.Lock()
		f.orgDeletes = append(f.orgDeletes, r.PathValue("id"))
		f.mu.Unlock()
		writeJSON(w, 204, nil)
	})

	// Blocking-member count for a delete.
	mux.HandleFunc("GET /api/collections/users/records", func(w http.ResponseWriter, r *http.Request) {
		f.mu.Lock()
		defer f.mu.Unlock()
		writeJSON(w, 200, map[string]any{"totalItems": f.memberCount, "items": []any{}})
	})

	// Caller promotion / demotion.
	mux.HandleFunc("PATCH /api/collections/users/records/{id}", func(w http.ResponseWriter, r *http.Request) {
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		f.mu.Lock()
		f.userPatches = append(f.userPatches, body)
		status := f.userPatchStatus
		f.mu.Unlock()
		if status != 0 && status != 200 {
			writeJSON(w, status, map[string]any{"message": "cannot update user"})
			return
		}
		writeJSON(w, 200, map[string]any{"id": r.PathValue("id")})
	})

	return mux
}

// newOrgTestServer wires the fake PocketBase to a real Handler and returns the
// app's base URL.
func newOrgTestServer(t *testing.T, f *orgFakePB) string {
	t.Helper()
	pbSrv := httptest.NewServer(f.handler(t))
	t.Cleanup(pbSrv.Close)

	s := New(config.Config{UsersCollection: "users"}, pb.New(pbSrv.URL, "admin@test.local", "pw"))
	appSrv := httptest.NewServer(s.Handler())
	t.Cleanup(appSrv.Close)
	return appSrv.URL
}

// orgReq issues an authenticated request and returns the status and decoded body.
func orgReq(t *testing.T, base, method, path string, body any) (int, map[string]any) {
	t.Helper()
	var buf bytes.Buffer
	if body != nil {
		if err := json.NewEncoder(&buf).Encode(body); err != nil {
			t.Fatal(err)
		}
	}
	req, err := http.NewRequest(method, base+path, &buf)
	if err != nil {
		t.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer "+orgTestBearer)
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	out := map[string]any{}
	_ = json.NewDecoder(resp.Body).Decode(&out)
	return resp.StatusCode, out
}

func TestCreateOrgPromotesCreatorToAdmin(t *testing.T) {
	f := &orgFakePB{callerRole: roleUser, callerOrg: ""}
	base := newOrgTestServer(t, f)

	status, body := orgReq(t, base, http.MethodPost, "/api/orgs", map[string]any{"name": "Acme Inc."})
	if status != http.StatusCreated {
		t.Fatalf("status = %d, want 201 (body %v)", status, body)
	}
	org, _ := body["organization"].(map[string]any)
	if org["id"] != orgTestNewID || org["name"] != "Acme Inc." {
		t.Fatalf("organization = %v, want id %q name %q", org, orgTestNewID, "Acme Inc.")
	}

	if len(f.userPatches) != 1 {
		t.Fatalf("user patches = %d, want 1 (%v)", len(f.userPatches), f.userPatches)
	}
	patch := f.userPatches[0]
	if patch["organization"] != orgTestNewID {
		t.Errorf("patched organization = %v, want %q", patch["organization"], orgTestNewID)
	}
	if patch["role"] != roleAdmin {
		t.Errorf("patched role = %v, want %q", patch["role"], roleAdmin)
	}
	if len(f.orgDeletes) != 0 {
		t.Errorf("org was rolled back unexpectedly: %v", f.orgDeletes)
	}
}

func TestCreateOrgRejectedWhenCallerAlreadyHasOne(t *testing.T) {
	f := &orgFakePB{callerRole: roleUser, callerOrg: "existing1"}
	base := newOrgTestServer(t, f)

	status, body := orgReq(t, base, http.MethodPost, "/api/orgs", map[string]any{"name": "Second Org"})
	if status != http.StatusForbidden {
		t.Fatalf("status = %d, want 403 (body %v)", status, body)
	}
	if len(f.createdOrgs) != 0 {
		t.Errorf("created orgs = %v, want none", f.createdOrgs)
	}
	if len(f.userPatches) != 0 {
		t.Errorf("user patches = %v, want none", f.userPatches)
	}
}

// A superadmin manages every tenant centrally, so creating one must not move
// them into it or change their role.
func TestCreateOrgLeavesSuperadminUnattached(t *testing.T) {
	f := &orgFakePB{callerRole: roleSuperadmin, callerOrg: ""}
	base := newOrgTestServer(t, f)

	status, body := orgReq(t, base, http.MethodPost, "/api/orgs", map[string]any{"name": "Tenant A"})
	if status != http.StatusCreated {
		t.Fatalf("status = %d, want 201 (body %v)", status, body)
	}
	if len(f.userPatches) != 0 {
		t.Errorf("user patches = %v, want none for a superadmin", f.userPatches)
	}
}

// If the creator cannot be promoted, the new org must not survive — otherwise it
// is stranded with nobody able to administer it.
func TestCreateOrgRollsBackWhenPromotionFails(t *testing.T) {
	f := &orgFakePB{callerRole: roleUser, callerOrg: "", userPatchStatus: http.StatusBadRequest}
	base := newOrgTestServer(t, f)

	status, _ := orgReq(t, base, http.MethodPost, "/api/orgs", map[string]any{"name": "Doomed"})
	if status == http.StatusCreated {
		t.Fatalf("status = %d, want a failure", status)
	}
	if len(f.orgDeletes) != 1 || f.orgDeletes[0] != orgTestNewID {
		t.Errorf("org deletes = %v, want rollback of %q", f.orgDeletes, orgTestNewID)
	}
}

func TestCreateOrgRejectsBadName(t *testing.T) {
	f := &orgFakePB{callerRole: roleUser, callerOrg: ""}
	base := newOrgTestServer(t, f)

	status, _ := orgReq(t, base, http.MethodPost, "/api/orgs", map[string]any{"name": "   "})
	if status != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400", status)
	}
	if len(f.createdOrgs) != 0 {
		t.Errorf("created orgs = %v, want none", f.createdOrgs)
	}
}

func TestUpdateOrgAdminScopedToOwnOrg(t *testing.T) {
	f := &orgFakePB{callerRole: roleAdmin, callerOrg: "o1"}
	base := newOrgTestServer(t, f)

	// Someone else's organization is off limits.
	status, _ := orgReq(t, base, http.MethodPatch, "/api/orgs/o2", map[string]any{"name": "Hijack"})
	if status != http.StatusForbidden {
		t.Fatalf("foreign org status = %d, want 403", status)
	}
	if len(f.orgPatches) != 0 {
		t.Fatalf("org patches = %v, want none", f.orgPatches)
	}

	// Their own is fine.
	status, body := orgReq(t, base, http.MethodPatch, "/api/orgs/o1", map[string]any{"name": "Renamed"})
	if status != http.StatusOK {
		t.Fatalf("own org status = %d, want 200 (body %v)", status, body)
	}
	if len(f.orgPatches) != 1 || f.orgPatches[0]["name"] != "Renamed" {
		t.Errorf("org patches = %v, want one rename to %q", f.orgPatches, "Renamed")
	}
}

// An admin deleting their own org is detached and demoted first, so the org is
// empty by the time it is removed.
func TestDeleteOrgAdminSoleMemberIsDetachedAndDemoted(t *testing.T) {
	f := &orgFakePB{callerRole: roleAdmin, callerOrg: "o1", memberCount: 0}
	base := newOrgTestServer(t, f)

	status, body := orgReq(t, base, http.MethodDelete, "/api/orgs/o1", nil)
	if status != http.StatusOK {
		t.Fatalf("status = %d, want 200 (body %v)", status, body)
	}
	if len(f.userPatches) != 1 {
		t.Fatalf("user patches = %d, want 1 (%v)", len(f.userPatches), f.userPatches)
	}
	patch := f.userPatches[0]
	if patch["organization"] != "" {
		t.Errorf("patched organization = %v, want cleared", patch["organization"])
	}
	if patch["role"] != roleUser {
		t.Errorf("patched role = %v, want %q", patch["role"], roleUser)
	}
	if len(f.orgDeletes) != 1 || f.orgDeletes[0] != "o1" {
		t.Errorf("org deletes = %v, want [o1]", f.orgDeletes)
	}
}

func TestDeleteOrgBlockedByOtherMembers(t *testing.T) {
	f := &orgFakePB{callerRole: roleAdmin, callerOrg: "o1", memberCount: 2}
	base := newOrgTestServer(t, f)

	status, _ := orgReq(t, base, http.MethodDelete, "/api/orgs/o1", nil)
	if status != http.StatusConflict {
		t.Fatalf("status = %d, want 409", status)
	}
	if len(f.orgDeletes) != 0 {
		t.Errorf("org deletes = %v, want none", f.orgDeletes)
	}
	if len(f.userPatches) != 0 {
		t.Errorf("user patches = %v, want none — the admin must stay put", f.userPatches)
	}
}

func TestDeleteOrgAdminCannotDeleteForeignOrg(t *testing.T) {
	f := &orgFakePB{callerRole: roleAdmin, callerOrg: "o1"}
	base := newOrgTestServer(t, f)

	status, _ := orgReq(t, base, http.MethodDelete, "/api/orgs/o2", nil)
	if status != http.StatusForbidden {
		t.Fatalf("status = %d, want 403", status)
	}
	if len(f.orgDeletes) != 0 {
		t.Errorf("org deletes = %v, want none", f.orgDeletes)
	}
}

// A plain user is not a manager, so rename/delete stay closed to them even for
// the org they belong to.
func TestPlainUserCannotManageOrgs(t *testing.T) {
	f := &orgFakePB{callerRole: roleUser, callerOrg: "o1"}
	base := newOrgTestServer(t, f)

	if status, _ := orgReq(t, base, http.MethodPatch, "/api/orgs/o1", map[string]any{"name": "Nope"}); status != http.StatusForbidden {
		t.Errorf("rename status = %d, want 403", status)
	}
	if status, _ := orgReq(t, base, http.MethodDelete, "/api/orgs/o1", nil); status != http.StatusForbidden {
		t.Errorf("delete status = %d, want 403", status)
	}
}
