//go:build e2e

// Package testkit provides E2E-specific test harnesses shared across the
// e2e test suite. It is compiled with the //go:build e2e tag.
package testkit

import (
	"context"
	"database/sql"
	"fmt"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

// OwnershipFixture is the IDs and visible state a writer/backfill/fail-closed
// test needs to drive the production Store against a REAL ownership graph:
// every workspace, machine, member and user row the GREEN migration's
// preflight is expected to validate against.
type OwnershipFixture struct {
	UserID         string
	UserName       string
	OrganizationID string
	WorkspaceID    string
	WorkspaceSlug  string
	MachineID      string
	MachineName    string
	OwnerMemberID  string

	// AuthorUserID / AuthorMemberID carry an INDEPENDENT active-human user +
	// member pair installed by SeedReviewAuthor. Blocker tests use this pair
	// as the legitimate Review audit author while varying only the machine
	// creator's member state — keeping the Review row's created_by_member_id
	// resolvable regardless of which owner-resolution case is under test.
	AuthorUserID   string
	AuthorMemberID string
}

// SeedWorkspaceWithMachine builds the minimal workspace → machine ownership
// graph (user → organization → workspace → machine) and returns the IDs. The
// machine's created_by_user_id points at the seeded user, but NO member row
// is inserted — callers must invoke one of SeedActiveHumanMember /
// SeedInactiveHumanMember / SeedAgentMember to establish the owner-resolution
// state the GREEN preflight will consult.
//
// This split lets fail-closed tests construct a workspace+machine whose
// creator has missing/inactive/non-human member state without touching the
// happy-path helper below.
func SeedWorkspaceWithMachine(t *testing.T, ctx context.Context, db *sql.DB, tag string) OwnershipFixture {
	t.Helper()

	if ctx == nil {
		ctx = context.Background()
	}
	if db == nil {
		t.Fatal("SeedWorkspaceWithMachine: db is nil")
	}
	if tag == "" {
		t.Fatal("SeedWorkspaceWithMachine: tag is required")
	}

	uid := uniqueSuffix()
	fixture := OwnershipFixture{
		UserID:         "usr_" + tag + "_" + uid,
		UserName:       "user-" + tag + "-" + uid,
		OrganizationID: "org_" + tag + "_" + uid,
		WorkspaceID:    "wsp_" + tag + "_" + uid,
		WorkspaceSlug:  "ws-" + tag + "-" + uid,
		MachineID:      "mac_" + tag + "_" + uid,
		MachineName:    "machine-" + tag + "-" + uid,
		// OwnerMemberID intentionally empty — caller seeds the member state.
	}

	now := time.Now().UTC()

	_, err := db.ExecContext(ctx, `
INSERT INTO users (id, name, display_name, password_hash, created_at, updated_at)
VALUES (?, ?, ?, 'x', ?, ?)
`, fixture.UserID, fixture.UserName, "User "+tag, now, now)
	require.NoError(t, err, "seed user for %s", tag)

	_, err = db.ExecContext(ctx, `
INSERT INTO organizations (id, slug, name, created_by_user_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`, fixture.OrganizationID, fixture.WorkspaceSlug, "Org "+tag, fixture.UserID, now, now)
	require.NoError(t, err, "seed organization for %s", tag)

	_, err = db.ExecContext(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`, fixture.WorkspaceID, fixture.OrganizationID, fixture.WorkspaceSlug, "Workspace "+tag, now, now)
	require.NoError(t, err, "seed workspace for %s", tag)

	_, err = db.ExecContext(ctx, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'active', ?, ?)
`, fixture.MachineID, fixture.WorkspaceID, fixture.UserID, fixture.MachineName, "key_"+uid, now, now)
	require.NoError(t, err, "seed machine for %s", tag)

	return fixture
}

// SeedActiveHumanMember inserts a 'human' member with status='active' and
// role='owner' for the given workspace + user. Returns the new member id.
//
// This is the legitimate owner the GREEN owner preflight expects to resolve
// from machine.created_by_user_id.
func SeedActiveHumanMember(t *testing.T, ctx context.Context, db *sql.DB, ws OwnershipFixture, tag string) string {
	t.Helper()

	memberID := "mem_" + tag + "_" + uniqueSuffix()
	now := time.Now().UTC()
	_, err := db.ExecContext(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES (?, ?, 'human', ?, ?, 'owner', 'active', ?, ?)
`, memberID, ws.WorkspaceID, "Owner "+tag, ws.UserID, now, now)
	require.NoError(t, err, "seed active-human member for %s", tag)
	return memberID
}

// SeedInactiveHumanMember inserts a 'human' member with status='inactive'.
// Used by fail-closed cases that must distinguish "user is in the workspace
// but deactivated" from "user has no member row at all". Returns the new
// member id.
func SeedInactiveHumanMember(t *testing.T, ctx context.Context, db *sql.DB, ws OwnershipFixture, tag string) string {
	t.Helper()

	memberID := "mem_inactive_" + tag + "_" + uniqueSuffix()
	now := time.Now().UTC()
	_, err := db.ExecContext(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES (?, ?, 'human', ?, ?, 'member', 'inactive', ?, ?)
`, memberID, ws.WorkspaceID, "Inactive "+tag, ws.UserID, now, now)
	require.NoError(t, err, "seed inactive-human member for %s", tag)
	return memberID
}

// SeedAgentMember inserts an 'agent' member with status='active'. Used by
// fail-closed cases that must distinguish "the only member bound to
// machine.created_by_user_id is a non-human agent" from "an active human
// owner exists". Returns the new member id.
func SeedAgentMember(t *testing.T, ctx context.Context, db *sql.DB, ws OwnershipFixture, tag string) string {
	t.Helper()

	memberID := "mem_agent_" + tag + "_" + uniqueSuffix()
	now := time.Now().UTC()
	_, err := db.ExecContext(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES (?, ?, 'agent', ?, ?, 'member', 'active', ?, ?)
`, memberID, ws.WorkspaceID, "Agent "+tag, ws.UserID, now, now)
	require.NoError(t, err, "seed agent member for %s", tag)
	return memberID
}

// SeedWorkspaceWithMachineAndOwner is the legal-baseline shortcut: build the
// workspace+machine graph via SeedWorkspaceWithMachine, then attach an active
// human owner member. Returns the full OwnershipFixture with OwnerMemberID
// populated.
//
// Happy-path tests (Backfill, writer create/upsert/update, concurrency)
// use this so the GREEN owner preflight has a legitimate member to wire.
func SeedWorkspaceWithMachineAndOwner(t *testing.T, ctx context.Context, db *sql.DB, tag string) OwnershipFixture {
	t.Helper()
	ws := SeedWorkspaceWithMachine(t, ctx, db, tag)
	ws.OwnerMemberID = SeedActiveHumanMember(t, ctx, db, ws, tag)
	return ws
}

// SeedReviewAuthor installs a SEPARATE active-human user+member pair to act as
// the legitimate Review audit author in blocker tests. The pair is independent
// of the machine's created_by_user_id so the caller may vary the
// machine-creator's member state (missing / inactive / agent) without
// invalidating the Review row's created_by_member_id / updated_by_member_id.
//
// Returns (authorUserID, authorMemberID). The caller is responsible for
// stashing them on the fixture (e.g., ws.AuthorUserID / ws.AuthorMemberID)
// before passing the fixture downstream — the function takes the fixture by
// value and does not mutate the caller's copy.
func SeedReviewAuthor(t *testing.T, ctx context.Context, db *sql.DB, ws OwnershipFixture, tag string) (string, string) {
	t.Helper()

	uid := uniqueSuffix()
	userID := "usr_author_" + tag + "_" + uid
	memberID := "mem_author_" + tag + "_" + uid
	now := time.Now().UTC()

	_, err := db.ExecContext(ctx, `
INSERT INTO users (id, name, display_name, password_hash, created_at, updated_at)
VALUES (?, ?, ?, 'x', ?, ?)
`, userID, "author-"+tag+"-"+uid, "Author "+tag, now, now)
	require.NoError(t, err, "seed review-author user for %s", tag)

	_, err = db.ExecContext(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES (?, ?, 'human', ?, ?, 'owner', 'active', ?, ?)
`, memberID, ws.WorkspaceID, "Author "+tag, userID, now, now)
	require.NoError(t, err, "seed review-author member for %s", tag)

	return userID, memberID
}

var uniqueCounter atomic.Uint64

// uniqueSuffix returns a process-unique, monotonically-increasing identifier
// suitable for appending to seed IDs. atomic.Uint64 keeps parallel subtests
// from duplicating IDs without contending on a mutex.
func uniqueSuffix() string {
	n := uniqueCounter.Add(1)
	return fmt.Sprintf("%d%d", time.Now().UnixNano(), n)
}
