package rls

import (
	"strings"
	"testing"
)

// transitivelyUnrestricted builds a Result that renders to "" without having
// Skip set: its own ParentFilter points at a Skip parent, so ToSQLFilter
// collapses it to unrestricted one level down. This is the shape a
// ControlledByParent middle object takes when the chain root is unrestricted
// via object-level ViewAllRecords (issue #3845).
func transitivelyUnrestricted(objectAPIName string) *Result {
	return &Result{
		ObjectAPIName: objectAPIName,
		ParentFilter: &ParentFilter{
			ParentObjectAPIName: "RootObj",
			ForeignKeyField:     "RootRef",
			ParentRLSResult:     &Result{Skip: true, ObjectAPIName: "RootObj"},
			ParentDerivedOnly:   true, // required FK edge
		},
	}
}

// A parent whose filter collapses to "" (transitively unrestricted, Skip=false)
// must make the child unrestricted too — not silently lose the parent arm
// (issue #3845: with a parentOnly edge the loss degenerated to "1 = 0").
func TestToSQLFilterParentArmTransitivelyUnrestrictedParentInheritsUnrestricted(t *testing.T) {
	parent := transitivelyUnrestricted("MidObj")
	if got := ToSQLFilter(parent, ""); got != "" {
		t.Fatalf("fixture: expected transitively-unrestricted parent to render to \"\", got %q", got)
	}
	if parent.Skip {
		t.Fatalf("fixture: parent must be unrestricted WITHOUT Skip for this regression to bite")
	}

	child := &Result{
		ObjectAPIName: "LeafObj",
		ParentFilter: &ParentFilter{
			ParentObjectAPIName: "MidObj",
			ForeignKeyField:     "MidRef",
			ParentRLSResult:     parent,
			ParentDerivedOnly:   true, // required FK edge
		},
	}
	if got := ToSQLFilter(child, ""); got != "" {
		t.Fatalf("child of a fully-visible parent set must be unrestricted, got %q", got)
	}
}

// Optional edge whose fallback arms happen to render EMPTY for this principal
// (no OwnerId field, proven-empty share/team probes): arm-count is not a
// proxy for the edge kind. The filter must still be the unfiltered parent
// arm — never "" — so standalone/orphan rows stay invisible, symmetric with
// record access denying them after the parent lookup falls through.
func TestToSQLFilterOptionalEdgeEmptyFallbacksStillEmitParentArm(t *testing.T) {
	child := &Result{
		ObjectAPIName: "LeafObj",
		// No owner field, no share/team arms, no criteria: nothing but the edge.
		ParentFilter: &ParentFilter{
			ParentObjectAPIName: "MidObj",
			ForeignKeyField:     "MidRef",
			ParentRLSResult:     transitivelyUnrestricted("MidObj"),
			ParentDerivedOnly:   false, // optional edge
		},
	}
	got := ToSQLFilter(child, "")
	if got != "`MidRef` IN (SELECT Id FROM `MidObj`)" {
		t.Fatalf("optional edge with empty fallbacks must render exactly the unfiltered parent arm, got %q", got)
	}
}

// Optional-edge shape: the child keeps owner/share fallback arms for
// standalone/orphan rows. An unrestricted parent set (transitive here) must
// NOT short-circuit the whole filter to "" — parent-linked rows are granted
// via an unfiltered parent arm while orphans stay gated by the fallbacks.
func TestToSQLFilterOptionalEdgeUnrestrictedParentKeepsOrphanFallbackArms(t *testing.T) {
	for _, tc := range []struct {
		name   string
		parent *Result
	}{
		{"transitivelyUnrestrictedParent", transitivelyUnrestricted("MidObj")},
		{"directSkipParent", &Result{Skip: true, ObjectAPIName: "MidObj"}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			child := &Result{
				ObjectAPIName: "LeafObj",
				HasOwnerField: true,
				UserID:        "005-user",
				OwnerUserIDs:  []string{"005-user"},
				ParentFilter: &ParentFilter{
					ParentObjectAPIName: "MidObj",
					ForeignKeyField:     "MidRef",
					ParentRLSResult:     tc.parent,
				},
			}
			got := ToSQLFilter(child, "")
			if got == "" {
				t.Fatalf("optional edge must not collapse to unrestricted — orphan rows would bypass owner/share gating")
			}
			if !strings.Contains(got, "`MidRef` IN (SELECT Id FROM `MidObj`)") {
				t.Fatalf("parent-linked rows must be granted via the unfiltered parent arm, got %q", got)
			}
			if !strings.Contains(got, "`OwnerId` = '005-user'") {
				t.Fatalf("owner fallback arm must survive for orphan rows, got %q", got)
			}
		})
	}
}

// A nil parent result carries no visibility statement: the parent arm stays
// dropped and, with no other arms, the filter fails closed.
func TestToSQLFilterParentArmNilParentResultStaysFailClosed(t *testing.T) {
	child := &Result{
		ObjectAPIName: "LeafObj",
		ParentFilter: &ParentFilter{
			ParentObjectAPIName: "MidObj",
			ForeignKeyField:     "MidRef",
			ParentRLSResult:     nil,
		},
	}
	if got := ToSQLFilter(child, ""); got != "1 = 0" {
		t.Fatalf("nil parent result must fail closed to \"1 = 0\", got %q", got)
	}
}

// A DenyAll parent renders to the non-empty "1 = 0", so the parent arm is
// emitted as a match-nothing subquery — never as unrestricted.
func TestToSQLFilterParentArmDenyAllParentKeepsFailClosedSubquery(t *testing.T) {
	child := &Result{
		ObjectAPIName: "LeafObj",
		ParentFilter: &ParentFilter{
			ParentObjectAPIName: "MidObj",
			ForeignKeyField:     "MidRef",
			ParentRLSResult:     &Result{DenyAll: true, ObjectAPIName: "MidObj"},
		},
	}
	got := ToSQLFilter(child, "")
	if got == "" {
		t.Fatalf("DenyAll parent must not make the child unrestricted")
	}
	if !strings.Contains(got, "`MidRef` IN (SELECT Id FROM `MidObj` WHERE 1 = 0)") {
		t.Fatalf("DenyAll parent must keep the match-nothing parent subquery, got %q", got)
	}
}

// The TargetRecordFilters branch has the same conflation shape: a target whose
// RLS collapsed transitively to "" must be treated like a Skip target (arm
// kept, deleted-row filtering still applied), not dropped.
func TestToSQLFilterTargetRecordTransitivelyUnrestrictedTargetKeptWithDeletedFilter(t *testing.T) {
	got := ToSQLFilter(&Result{
		ObjectAPIName: "HistoryLike",
		TargetRecordFilters: []TargetRecordFilter{{
			TargetObjectAPIName:   "MidObj",
			TargetObjectField:     "TargetObjectType",
			TargetRecordField:     "TargetObjectId",
			TargetReadTable:       "v_MidObj",
			TargetIDColumn:        "Id",
			TargetIsDeletedColumn: "IsDeleted",
			TargetRLSResult:       transitivelyUnrestricted("MidObj"),
			TargetIncludeDeleted:  false,
		}},
	}, "HistoryLike")

	required := []string{
		"`HistoryLike`.`TargetObjectType` = 'MidObj'",
		"EXISTS (SELECT 1 FROM `v_MidObj` WHERE `v_MidObj`.`Id` = `HistoryLike`.`TargetObjectId` AND `v_MidObj`.`IsDeleted` = 0)",
	}
	for _, want := range required {
		if !strings.Contains(got, want) {
			t.Fatalf("transitively-unrestricted target arm missing %q\nSQL:\n%s", want, got)
		}
	}
}

// A nil target result stays fail-closed: the arm is dropped entirely.
func TestToSQLFilterTargetRecordNilTargetResultDropsArm(t *testing.T) {
	got := ToSQLFilter(&Result{
		ObjectAPIName: "HistoryLike",
		TargetRecordFilters: []TargetRecordFilter{{
			TargetObjectAPIName: "MidObj",
			TargetObjectField:   "TargetObjectType",
			TargetRecordField:   "TargetObjectId",
			TargetRLSResult:     nil,
		}},
	}, "HistoryLike")
	if got != "1 = 0" {
		t.Fatalf("nil target result must drop the arm and fail closed, got %q", got)
	}
}
