package services

import (
	"context"
	"os"
	"testing"
	"time"

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

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

// TestCanSee covers the two new visibility-only gates added for t-paliad-091:
//
//   - ProjectService.CanSee
//   - AppointmentService.CanSee
//
// Both replace cross-service GetByID-for-visibility calls in NoteService
// with a single EXISTS round-trip. The acceptance criteria from the task
// spec are: admin sees everything; team-member sees their team's;
// non-member sees nothing.
//
// Skipped when TEST_DATABASE_URL is unset, mirroring the other live-DB tests.
func TestCanSee(t *testing.T) {
	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)
	}
	defer pool.Close()

	ctx := context.Background()

	adminID := uuid.New()
	memberID := uuid.New()
	outsiderID := uuid.New()
	projectID := uuid.New()
	personalAppointmentID := uuid.New()
	projectAppointmentID := uuid.New()

	cleanup := func() {
		pool.ExecContext(ctx, `DELETE FROM paliad.appointments  WHERE id IN ($1, $2)`, personalAppointmentID, projectAppointmentID)
		pool.ExecContext(ctx, `DELETE FROM paliad.project_teams WHERE project_id = $1`, projectID)
		pool.ExecContext(ctx, `DELETE FROM paliad.projects      WHERE id = $1`, projectID)
		pool.ExecContext(ctx, `DELETE FROM paliad.users         WHERE id IN ($1, $2, $3)`, adminID, memberID, outsiderID)
		pool.ExecContext(ctx, `DELETE FROM auth.users           WHERE id IN ($1, $2, $3)`, adminID, memberID, outsiderID)
	}
	cleanup()
	defer cleanup()

	if _, err := pool.ExecContext(ctx,
		`INSERT INTO auth.users (id, email)
		 VALUES ($1, $2), ($3, $4), ($5, $6)`,
		adminID, "cansee-admin@hlc.com",
		memberID, "cansee-member@hlc.com",
		outsiderID, "cansee-outsider@hlc.com"); 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, 'CanSee Admin',    'munich', 'global_admin', 'de'),
		        ($3, $4, 'CanSee Member',   'munich', 'standard',     'de'),
		        ($5, $6, 'CanSee Outsider', 'munich', 'standard',     'de')`,
		adminID, "cansee-admin@hlc.com",
		memberID, "cansee-member@hlc.com",
		outsiderID, "cansee-outsider@hlc.com"); err != nil {
		t.Fatalf("seed paliad.users: %v", err)
	}

	// Project owned by adminID. memberID joins the project team; outsiderID
	// stays out. project_teams intentionally has no admin row so the
	// global_admin shortcut is what makes adminID see it.
	if _, err := pool.ExecContext(ctx,
		`INSERT INTO paliad.projects (id, type, path, title, reference, status, created_by)
		 VALUES ($1, 'project', $1::text, 'CanSee Project', '2026/9991', 'active', $2)`,
		projectID, adminID); err != nil {
		t.Fatalf("seed paliad.projects: %v", err)
	}
	if _, err := pool.ExecContext(ctx,
		`INSERT INTO paliad.project_teams (project_id, user_id, role)
		 VALUES ($1, $2, 'associate')`,
		projectID, memberID); err != nil {
		t.Fatalf("seed paliad.project_teams: %v", err)
	}

	// Personal appointment created by memberID — only memberID may see it
	// (admin's global_admin role does NOT cover personal appointments by
	// design, since they have no project anchor).
	startAt := time.Now().UTC().Add(48 * time.Hour)
	if _, err := pool.ExecContext(ctx,
		`INSERT INTO paliad.appointments
		   (id, project_id, title, start_at, appointment_type, created_by)
		 VALUES ($1, NULL, 'Personal CanSee', $2, 'meeting', $3)`,
		personalAppointmentID, startAt, memberID); err != nil {
		t.Fatalf("seed personal appointment: %v", err)
	}
	// Project-anchored appointment — visibility flows through the project.
	if _, err := pool.ExecContext(ctx,
		`INSERT INTO paliad.appointments
		   (id, project_id, title, start_at, appointment_type, created_by)
		 VALUES ($1, $2, 'Project CanSee', $3, 'meeting', $4)`,
		projectAppointmentID, projectID, startAt, adminID); err != nil {
		t.Fatalf("seed project appointment: %v", err)
	}

	users := NewUserService(pool)
	projects := NewProjectService(pool, users)
	appointments := NewAppointmentService(pool, projects)

	t.Run("ProjectService.CanSee", func(t *testing.T) {
		t.Run("global_admin sees project even without team membership", func(t *testing.T) {
			got, err := projects.CanSee(ctx, adminID, projectID)
			if err != nil {
				t.Fatalf("CanSee admin: %v", err)
			}
			if !got {
				t.Errorf("CanSee admin = false, want true (global_admin shortcut)")
			}
		})
		t.Run("team member sees project", func(t *testing.T) {
			got, err := projects.CanSee(ctx, memberID, projectID)
			if err != nil {
				t.Fatalf("CanSee member: %v", err)
			}
			if !got {
				t.Errorf("CanSee member = false, want true")
			}
		})
		t.Run("non-member does not see project", func(t *testing.T) {
			got, err := projects.CanSee(ctx, outsiderID, projectID)
			if err != nil {
				t.Fatalf("CanSee outsider: %v", err)
			}
			if got {
				t.Errorf("CanSee outsider = true, want false")
			}
		})
		t.Run("missing project is invisible to all", func(t *testing.T) {
			missing := uuid.New()
			for _, who := range []struct {
				name string
				id   uuid.UUID
			}{{"admin", adminID}, {"member", memberID}, {"outsider", outsiderID}} {
				got, err := projects.CanSee(ctx, who.id, missing)
				if err != nil {
					t.Fatalf("CanSee %s missing: %v", who.name, err)
				}
				if got {
					t.Errorf("CanSee %s missing = true, want false", who.name)
				}
			}
		})
	})

	t.Run("AppointmentService.CanSee", func(t *testing.T) {
		t.Run("project-anchored: global_admin sees", func(t *testing.T) {
			got, err := appointments.CanSee(ctx, adminID, projectAppointmentID)
			if err != nil {
				t.Fatalf("CanSee admin: %v", err)
			}
			if !got {
				t.Errorf("CanSee admin = false, want true")
			}
		})
		t.Run("project-anchored: team member sees", func(t *testing.T) {
			got, err := appointments.CanSee(ctx, memberID, projectAppointmentID)
			if err != nil {
				t.Fatalf("CanSee member: %v", err)
			}
			if !got {
				t.Errorf("CanSee member = false, want true")
			}
		})
		t.Run("project-anchored: non-member does not see", func(t *testing.T) {
			got, err := appointments.CanSee(ctx, outsiderID, projectAppointmentID)
			if err != nil {
				t.Fatalf("CanSee outsider: %v", err)
			}
			if got {
				t.Errorf("CanSee outsider = true, want false")
			}
		})
		t.Run("personal: only the creator sees it", func(t *testing.T) {
			got, err := appointments.CanSee(ctx, memberID, personalAppointmentID)
			if err != nil {
				t.Fatalf("CanSee member personal: %v", err)
			}
			if !got {
				t.Errorf("CanSee member personal = false, want true (own appointment)")
			}
			// global_admin does NOT see another user's personal appointment.
			got, err = appointments.CanSee(ctx, adminID, personalAppointmentID)
			if err != nil {
				t.Fatalf("CanSee admin personal: %v", err)
			}
			if got {
				t.Errorf("CanSee admin personal = true, want false (personal appointments are private)")
			}
			got, err = appointments.CanSee(ctx, outsiderID, personalAppointmentID)
			if err != nil {
				t.Fatalf("CanSee outsider personal: %v", err)
			}
			if got {
				t.Errorf("CanSee outsider personal = true, want false")
			}
		})
		t.Run("missing appointment is invisible to all", func(t *testing.T) {
			missing := uuid.New()
			for _, who := range []struct {
				name string
				id   uuid.UUID
			}{{"admin", adminID}, {"member", memberID}, {"outsider", outsiderID}} {
				got, err := appointments.CanSee(ctx, who.id, missing)
				if err != nil {
					t.Fatalf("CanSee %s missing: %v", who.name, err)
				}
				if got {
					t.Errorf("CanSee %s missing = true, want false", who.name)
				}
			}
		})
	})
}
