package services

// Live-DB tests for PinService and the BuildTreeWithOptions chip branches.
// Skipped when TEST_DATABASE_URL is unset.

import (
	"context"
	"errors"
	"os"
	"testing"

	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"

	"mgit.msbls.de/m/paliad/internal/db"
)

type pinTestEnv struct {
	t            *testing.T
	pool         *sqlx.DB
	pin          *PinService
	projects     *ProjectService
	userID       uuid.UUID
	otherUserID  uuid.UUID
	clientID     uuid.UUID // root, directly-staffed
	caseID       uuid.UUID // child of client, directly-staffed
	otherID      uuid.UUID // root, NOT directly-staffed (visible only to admin)
	cleanup      func()
}

func setupPinTest(t *testing.T) *pinTestEnv {
	t.Helper()
	url := os.Getenv("TEST_DATABASE_URL")
	if url == "" {
		t.Skip("TEST_DATABASE_URL not set — skipping live DB test")
	}
	if err := db.ApplyMigrations(url); err != nil {
		t.Fatalf("apply migrations: %v", err)
	}
	pool, err := sqlx.Connect("postgres", url)
	if err != nil {
		t.Fatalf("connect: %v", err)
	}
	ctx := context.Background()

	userID := uuid.New()      // standard user
	otherUserID := uuid.New() // separate user (used as creator of the "other" project)
	clientID := uuid.New()
	caseID := uuid.New()
	otherID := uuid.New()

	cleanup := func() {
		c := context.Background()
		pool.ExecContext(c, `DELETE FROM paliad.user_pinned_projects WHERE user_id IN ($1, $2)`, userID, otherUserID)
		for _, pid := range []uuid.UUID{caseID, clientID, otherID} {
			pool.ExecContext(c, `DELETE FROM paliad.project_teams  WHERE project_id = $1`, pid)
			pool.ExecContext(c, `DELETE FROM paliad.project_events WHERE project_id = $1`, pid)
		}
		pool.ExecContext(c, `DELETE FROM paliad.projects WHERE id IN ($1, $2)`, caseID, clientID)
		pool.ExecContext(c, `DELETE FROM paliad.projects WHERE id = $1`, otherID)
		pool.ExecContext(c, `DELETE FROM paliad.users WHERE id IN ($1, $2)`, userID, otherUserID)
		pool.ExecContext(c, `DELETE FROM auth.users  WHERE id IN ($1, $2)`, userID, otherUserID)
	}
	cleanup()

	for _, u := range []struct {
		id    uuid.UUID
		email string
		role  string
	}{
		{userID, "pin-test-user@hlc.com", "standard"},
		{otherUserID, "pin-test-other@hlc.com", "standard"},
	} {
		if _, err := pool.ExecContext(ctx,
			`INSERT INTO auth.users (id, email) VALUES ($1, $2)`, u.id, u.email); err != nil {
			t.Fatalf("seed auth.users: %v", err)
		}
		if _, err := pool.ExecContext(ctx,
			`INSERT INTO paliad.users (id, email, display_name, office, global_role, lang)
			 VALUES ($1, $2, 'Pin Test', 'munich', $3, 'de')`,
			u.id, u.email, u.role); err != nil {
			t.Fatalf("seed paliad.users: %v", err)
		}
	}

	// Build a small tree owned by userID (so userID is staffed direct on
	// clientID and caseID). otherID is owned by otherUserID — userID has
	// no visibility into it.
	for _, p := range []struct {
		id        uuid.UUID
		typ       string
		parent    *uuid.UUID
		title     string
		creator   uuid.UUID
		staff     uuid.UUID
	}{
		{clientID, "client", nil, "Pin Client", userID, userID},
		{caseID, "case", &clientID, "Pin Case", userID, userID},
		{otherID, "client", nil, "Other Client", otherUserID, otherUserID},
	} {
		var parent any
		if p.parent != nil {
			parent = *p.parent
		}
		if _, err := pool.ExecContext(ctx,
			`INSERT INTO paliad.projects (id, type, parent_id, path, title, status, created_by)
			 VALUES ($1, $2, $3, $4, $5, 'active', $6)`,
			p.id, p.typ, parent, p.id.String(), p.title, p.creator); err != nil {
			t.Fatalf("seed paliad.projects %s: %v", p.id, err)
		}
		if _, err := pool.ExecContext(ctx,
			`INSERT INTO paliad.project_teams (project_id, user_id, responsibility, inherited, added_by)
			 VALUES ($1, $2, 'lead', false, $2)`, p.id, p.staff); err != nil {
			t.Fatalf("seed project_teams %s: %v", p.id, err)
		}
	}

	users := NewUserService(pool)
	projects := NewProjectService(pool, users)
	pin := NewPinService(pool, projects)

	return &pinTestEnv{
		t:           t,
		pool:        pool,
		pin:         pin,
		projects:    projects,
		userID:      userID,
		otherUserID: otherUserID,
		clientID:    clientID,
		caseID:      caseID,
		otherID:     otherID,
		cleanup:     func() { cleanup(); pool.Close() },
	}
}

func TestPinService_PinAndIsPinned(t *testing.T) {
	env := setupPinTest(t)
	defer env.cleanup()
	ctx := context.Background()

	// Initially not pinned.
	if pinned, err := env.pin.IsPinned(ctx, env.userID, env.clientID); err != nil || pinned {
		t.Fatalf("IsPinned before = (%v, %v); want (false, nil)", pinned, err)
	}

	// Pin succeeds and is idempotent.
	if err := env.pin.Pin(ctx, env.userID, env.clientID); err != nil {
		t.Fatalf("Pin: %v", err)
	}
	if err := env.pin.Pin(ctx, env.userID, env.clientID); err != nil {
		t.Fatalf("Pin (idempotent): %v", err)
	}

	// IsPinned now true.
	if pinned, err := env.pin.IsPinned(ctx, env.userID, env.clientID); err != nil || !pinned {
		t.Fatalf("IsPinned after = (%v, %v); want (true, nil)", pinned, err)
	}

	// PinnedSet contains exactly one entry.
	set, err := env.pin.PinnedSet(ctx, env.userID)
	if err != nil {
		t.Fatalf("PinnedSet: %v", err)
	}
	if len(set) != 1 {
		t.Errorf("PinnedSet size = %d, want 1", len(set))
	}
	if _, ok := set[env.clientID]; !ok {
		t.Errorf("PinnedSet missing clientID")
	}

	// ListPinned returns slice form.
	ids, err := env.pin.ListPinned(ctx, env.userID)
	if err != nil {
		t.Fatalf("ListPinned: %v", err)
	}
	if len(ids) != 1 || ids[0] != env.clientID {
		t.Errorf("ListPinned = %v, want [clientID]", ids)
	}
}

func TestPinService_PinInvisible(t *testing.T) {
	env := setupPinTest(t)
	defer env.cleanup()
	ctx := context.Background()

	// userID cannot see otherID — Pin must return ErrNotVisible.
	if err := env.pin.Pin(ctx, env.userID, env.otherID); !errors.Is(err, ErrNotVisible) {
		t.Fatalf("Pin invisible = %v, want ErrNotVisible", err)
	}
}

func TestPinService_UnpinIdempotent(t *testing.T) {
	env := setupPinTest(t)
	defer env.cleanup()
	ctx := context.Background()

	// Unpin without prior pin = no-op.
	if err := env.pin.Unpin(ctx, env.userID, env.clientID); err != nil {
		t.Fatalf("Unpin (never pinned): %v", err)
	}

	// Pin then unpin twice; no error.
	if err := env.pin.Pin(ctx, env.userID, env.clientID); err != nil {
		t.Fatalf("Pin: %v", err)
	}
	if err := env.pin.Unpin(ctx, env.userID, env.clientID); err != nil {
		t.Fatalf("Unpin: %v", err)
	}
	if err := env.pin.Unpin(ctx, env.userID, env.clientID); err != nil {
		t.Fatalf("Unpin (idempotent): %v", err)
	}

	if pinned, _ := env.pin.IsPinned(ctx, env.userID, env.clientID); pinned {
		t.Errorf("IsPinned after double-unpin = true, want false")
	}
}

func TestPinService_OwnerScope(t *testing.T) {
	env := setupPinTest(t)
	defer env.cleanup()
	ctx := context.Background()

	// User A pins. User B's PinnedSet must be empty (RLS / explicit user_id).
	if err := env.pin.Pin(ctx, env.userID, env.clientID); err != nil {
		t.Fatalf("Pin: %v", err)
	}

	otherSet, err := env.pin.PinnedSet(ctx, env.otherUserID)
	if err != nil {
		t.Fatalf("PinnedSet (other user): %v", err)
	}
	if len(otherSet) != 0 {
		t.Errorf("other user PinnedSet size = %d, want 0", len(otherSet))
	}
}

func TestBuildTreeWithOptions_PinnedSetSurfaces(t *testing.T) {
	env := setupPinTest(t)
	defer env.cleanup()
	ctx := context.Background()

	if err := env.pin.Pin(ctx, env.userID, env.clientID); err != nil {
		t.Fatalf("Pin: %v", err)
	}
	set, _ := env.pin.PinnedSet(ctx, env.userID)

	tree, err := env.projects.BuildTreeWithOptions(ctx, env.userID, BuildTreeOptions{
		PinnedSet:            set,
		IncludeSubtreeCounts: true,
	})
	if err != nil {
		t.Fatalf("BuildTreeWithOptions: %v", err)
	}
	if len(tree) == 0 {
		t.Fatalf("tree empty; want clientID at root")
	}

	var found *ProjectTreeNode
	for _, r := range tree {
		if r.ID == env.clientID {
			found = r
			break
		}
	}
	if found == nil {
		t.Fatalf("clientID not at root; tree=%+v", tree)
	}
	if !found.Pinned {
		t.Errorf("clientID Pinned=false, want true")
	}
	// Case sub-node should NOT be pinned (we only pinned the client).
	if len(found.Children) == 0 || found.Children[0].Pinned {
		t.Errorf("child node should not be Pinned; got %+v", found.Children)
	}
}

func TestBuildTreeWithOptions_ScopeMineGreysAncestors(t *testing.T) {
	env := setupPinTest(t)
	defer env.cleanup()
	ctx := context.Background()

	// Remove the direct staffing on clientID so caseID becomes the only
	// directly-staffed project under the client. Then ScopeMine must keep
	// clientID as a greyed ancestor (InheritedVisibility=true).
	if _, err := env.pool.ExecContext(ctx,
		`DELETE FROM paliad.project_teams WHERE project_id = $1 AND user_id = $2`,
		env.clientID, env.userID); err != nil {
		t.Fatalf("delete client staffing: %v", err)
	}

	tree, err := env.projects.BuildTreeWithOptions(ctx, env.userID, BuildTreeOptions{
		Scope: ScopeMine,
	})
	if err != nil {
		t.Fatalf("BuildTreeWithOptions ScopeMine: %v", err)
	}
	if len(tree) == 0 {
		t.Fatalf("ScopeMine tree empty; want clientID greyed-ancestor")
	}

	var client *ProjectTreeNode
	for _, r := range tree {
		if r.ID == env.clientID {
			client = r
			break
		}
	}
	if client == nil {
		t.Fatalf("clientID missing from ScopeMine tree")
	}
	if !client.InheritedVisibility {
		t.Errorf("clientID InheritedVisibility=false; want true (greyed ancestor)")
	}
	if len(client.Children) != 1 || client.Children[0].ID != env.caseID {
		t.Fatalf("expected single child caseID; got %+v", client.Children)
	}
	if client.Children[0].InheritedVisibility {
		t.Errorf("caseID is direct-staffed; should NOT have InheritedVisibility=true")
	}
}
