package access

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"
)

// ---------------------------------------------------------------------------
// Structural parity test for the single-source active-Agent authority
// predicates. AuthorizeActiveAgentChannelMembership (read JOIN) and
// LockActiveAgentChannelAuthority (ordered locks) MUST build from the same
// shared security-condition set. The test extracts every security condition
// (`type`, `status`, `archived_at`, `removed_at`, `effective_to_seq`,
// `visibility`) from each function's SQL and asserts the two sets are equal —
// an independent status/join predicate added to either path fails.
// ---------------------------------------------------------------------------

// securityConditionRe matches a single security condition like
// `status = 'active'` or `status <> 'archived'`.
var securityConditionRe = regexp.MustCompile(`(?i)(type|status|archived_at|removed_at|effective_to_seq|visibility)\s*(=|<>|IN)\s*(.*)`)

// securityConditionSet extracts the normalized set of security conditions from
// the SQL fragments. Each fragment is split on ` AND ` so every condition is
// captured independently (the regex cannot span multiple ANDs).
func securityConditionSet(sqlFragments []string) map[string]bool {
	set := map[string]bool{}
	for _, frag := range sqlFragments {
		parts := regexp.MustCompile(`(?i)\s+and\s+`).Split(frag, -1)
		for _, part := range parts {
			m := securityConditionRe.FindStringSubmatch(part)
			if m == nil {
				continue
			}
			norm := strings.ToLower(strings.Join(strings.Fields(m[1]+" "+m[2]+" "+m[3]), " "))
			norm = strings.TrimSuffix(norm, " for update")
			set[norm] = true
		}
	}
	return set
}

// readSource is the current AuthorizeActiveAgentChannelMembership source.
const readSource = `
SELECT s.id
FROM scopes s
JOIN scope_memberships sm
  ON sm.scope_id = s.id
 AND sm.member_id = $3
 AND sm.visibility IN ('public', 'private')
 AND sm.removed_at IS NULL
 AND sm.archived_at IS NULL
 AND sm.effective_to_seq IS NULL
JOIN members m
  ON m.workspace_id = $1
 AND m.id = $3
 AND m.type = 'agent'
 AND m.status = 'active'
JOIN agents a
  ON a.workspace_id = $1
 AND a.member_id = $3
JOIN machines mac
  ON mac.workspace_id = a.workspace_id
 AND mac.id = a.machine_id
 AND mac.status = 'active'
WHERE s.workspace_id = $1
  AND s.id = $2
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL
LIMIT 1`

// lockScopeSource / lockMembershipSource / lockMemberSource / lockAgentSource /
// lockMachineSource are the current LockActiveAgentChannelAuthority ordered
// queries.
const lockScopeSource = `
SELECT s.id
FROM scopes s
WHERE s.workspace_id = $1
  AND s.id = $2
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL
FOR UPDATE`

const lockMembershipSource = `
SELECT sm.member_id
FROM scope_memberships sm
WHERE sm.scope_id = $2
  AND sm.member_id = $3
  AND sm.visibility IN ('public', 'private')
  AND sm.removed_at IS NULL
  AND sm.archived_at IS NULL
  AND sm.effective_to_seq IS NULL
FOR UPDATE`

const lockMemberSource = `
SELECT m.id
FROM members m
WHERE m.workspace_id = $1
  AND m.id = $3
  AND m.type = 'agent'
  AND m.status = 'active'
FOR UPDATE`

const lockAgentSource = `
SELECT COALESCE(a.machine_id, '')
FROM agents a
WHERE a.workspace_id = $1
  AND a.member_id = $2
FOR UPDATE`

const lockMachineSource = `
SELECT mac.id
FROM machines mac
WHERE mac.workspace_id = $1
  AND mac.id = $2
  AND mac.status = 'active'
FOR UPDATE`

// TestAgentReadLockPredicateParity proves the read authorizer and the ordered
// locking fence carry IDENTICAL security-condition sets. If a status/type/
// archive/close/visibility fact is added to one path but not the other, this
// fails — an independent predicate cannot hide in either path.
func TestAgentReadLockPredicateParity(t *testing.T) {
	// The const values ARE the SQL (raw strings), so feed them directly.
	read := securityConditionSet([]string{readSource})
	lock := securityConditionSet([]string{lockScopeSource, lockMembershipSource, lockMemberSource, lockAgentSource, lockMachineSource})

	for cond := range read {
		if !lock[cond] {
			t.Errorf("read path carries security condition %q but the lock path does not — predicates must stay single-source", cond)
		}
	}
	for cond := range lock {
		if !read[cond] {
			t.Errorf("lock path carries security condition %q but the read path does not — predicates must stay single-source", cond)
		}
	}
	if len(read) == 0 || len(lock) == 0 {
		t.Fatal("parity test extracted no security conditions — the extraction regex or source constants are broken")
	}
}

// TestAgentReadLockPredicateParitySharedConstants proves both production paths
// reference the shared predicate constants (not independently re-inlined SQL).
// The read authorizer source in authorizer.go must reference each shared
// predicate constant.
func TestAgentReadLockPredicateParitySharedConstants(t *testing.T) {
	read, err := loadProductionSource("internal/store/access/authorizer.go", "AuthorizeActiveAgentChannelMembership")
	if err != nil {
		t.Skipf("cannot load read source for parity check: %v", err)
	}
	for _, constant := range []string{
		"activeAgentChannelScopePredicate",
		"activeAgentMembershipSecurity",
		"activeAgentMemberSecurity",
		"activeAgentSecurity",
		"activeMachineStatusSecurity",
	} {
		if !strings.Contains(read, constant) {
			t.Errorf("AuthorizeActiveAgentChannelMembership must reference shared predicate constant %q (single-source), but does not", constant)
		}
	}
}

func loadProductionSource(path string, fn string) (string, error) {
	// Resolve the repo root from the package dir (internal/store/access) upward
	// until the target file exists.
	dir, err := os.Getwd()
	if err != nil {
		return "", err
	}
	for {
		candidate := filepath.Join(dir, path)
		if _, statErr := os.Stat(candidate); statErr == nil {
			content, readErr := os.ReadFile(candidate)
			if readErr != nil {
				return "", readErr
			}
			return extractFuncSource(string(content), fn), nil
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return "", fmt.Errorf("production source %s not found above %s", path, dir)
		}
		dir = parent
	}
}

// extractFuncSource returns the source text of the function with the given name
// (a rough but sufficient extraction for the parity check).
func extractFuncSource(content string, fn string) string {
	idx := strings.Index(content, "func "+fn)
	if idx < 0 {
		// Method form: `func (c *checker) fn`.
		idx = strings.Index(content, "fn "+fn)
	}
	if idx < 0 {
		return ""
	}
	// Find the opening brace and walk to the matching close.
	brace := strings.Index(content[idx:], "{")
	if brace < 0 {
		return ""
	}
	start := idx + brace + 1
	depth := 1
	for i := start; i < len(content); i++ {
		switch content[i] {
		case '{':
			depth++
		case '}':
			depth--
			if depth == 0 {
				return content[idx : i+1]
			}
		}
	}
	return content[idx:]
}
