package connectors

import (
	"context"
	"encoding/json"
	"testing"
)

// ---------------------------------------------------------------------------
// extractDirectoryItems — pure-function tests
// ---------------------------------------------------------------------------

func TestExtractDirectoryItems_RootIsArray(t *testing.T) {
	body := []byte(`[{"id":"1"},{"id":"2"}]`)
	items, err := extractDirectoryItems(body, "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(items) != 2 {
		t.Fatalf("expected 2 items, got %d", len(items))
	}
}

func TestExtractDirectoryItems_NestedPath(t *testing.T) {
	body := []byte(`{"results":[{"id":"1"}]}`)
	items, err := extractDirectoryItems(body, "results")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(items) != 1 {
		t.Fatalf("expected 1 item, got %d", len(items))
	}
}

func TestExtractDirectoryItems_EmptyArray(t *testing.T) {
	body := []byte(`{"results":[]}`)
	items, err := extractDirectoryItems(body, "results")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(items) != 0 {
		t.Fatalf("expected 0 items, got %d", len(items))
	}
}

func TestExtractDirectoryItems_MissingKey_FailsClosed(t *testing.T) {
	body := []byte(`{}`)
	_, err := extractDirectoryItems(body, "results")
	if err == nil {
		t.Fatal("expected error for missing key, got nil")
	}
}

func TestExtractDirectoryItems_NonObjectElement_FailsClosed(t *testing.T) {
	body := []byte(`{"results":[{"id":"1"},42]}`)
	_, err := extractDirectoryItems(body, "results")
	if err == nil {
		t.Fatal("expected error for non-object element, got nil")
	}
}

func TestExtractDirectoryItems_UseNumberPreserved(t *testing.T) {
	// Numeric ids must arrive as json.Number (no float64 coercion / sci-notation).
	body := []byte(`{"results":[{"id":1234567}]}`)
	items, err := extractDirectoryItems(body, "results")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(items) != 1 {
		t.Fatalf("expected 1 item, got %d", len(items))
	}
	v, ok := items[0]["id"].(json.Number)
	if !ok {
		t.Fatalf("expected id to be json.Number, got %T", items[0]["id"])
	}
	if v.String() != "1234567" {
		t.Errorf("expected id=1234567, got %q", v.String())
	}
}

// ---------------------------------------------------------------------------
// directoryExtractDottedValue — selected cases
// ---------------------------------------------------------------------------

func TestDirectoryExtractDottedValue_MissingSegment_Error(t *testing.T) {
	root := map[string]interface{}{"a": map[string]interface{}{}}
	_, err := directoryExtractDottedValue(root, "a.b")
	if err == nil {
		t.Fatal("expected error for missing segment, got nil")
	}
}

func TestDirectoryExtractDottedValue_NullLeaf_Error(t *testing.T) {
	root := map[string]interface{}{"a": nil}
	_, err := directoryExtractDottedValue(root, "a")
	if err == nil {
		t.Fatal("expected error for null leaf, got nil")
	}
}

// ---------------------------------------------------------------------------
// authorizeDirectoryProfileDependency — unit tests (same package, unexported)
// ---------------------------------------------------------------------------

// TestAuthorizeDirectoryProfileDependency_SamePack_NilDB_ReturnsNil confirms
// that same-pack (case-insensitive) passes without any DB access — nil db is
// safe for the same-pack path.
func TestAuthorizeDirectoryProfileDependency_SamePack_NilDB_ReturnsNil(t *testing.T) {
	adapter := &directoryReaderAdapter{db: nil, engine: nil}
	if err := adapter.authorizeDirectoryProfileDependency(context.Background(), "pack-a", "Pack-A"); err != nil {
		t.Errorf("same-pack with nil db must return nil, got: %v", err)
	}
}

// TestAuthorizeDirectoryProfileDependency_CrossPack_NilDB_ReturnsError confirms
// that cross-pack with a nil db returns an error containing "db is required".
func TestAuthorizeDirectoryProfileDependency_CrossPack_NilDB_ReturnsError(t *testing.T) {
	adapter := &directoryReaderAdapter{db: nil, engine: nil}
	err := adapter.authorizeDirectoryProfileDependency(context.Background(), "pack-a", "pack-b")
	if err == nil {
		t.Fatal("expected error for cross-pack with nil db, got nil")
	}
	if got := err.Error(); !containsSubstr(got, "db is required") {
		t.Errorf("expected error to mention 'db is required', got: %q", got)
	}
}

// ---------------------------------------------------------------------------
// PullDirectory integration guard — exercises the full PullDirectory path
// ---------------------------------------------------------------------------

// TestPullDirectory_CrossPack_NilDB_ReturnsError confirms that PullDirectory
// returns an error for cross-pack + nil db (no DB access attempted).
func TestPullDirectory_CrossPack_NilDB_ReturnsError(t *testing.T) {
	adapter := &directoryReaderAdapter{db: nil, engine: nil}
	_, err := adapter.PullDirectory(context.Background(), DirectoryPullRequest{
		RequesterOwnerPack: "pack-a",
		ProfileOwnerPack:   "pack-b",
		ProfileName:        "some-profile",
		ObjectKey:          "contacts",
		ItemsPath:          "items",
	})
	if err == nil {
		t.Fatal("expected cross-pack error, got nil")
	}
	if got := err.Error(); len(got) == 0 {
		t.Errorf("error message is empty")
	}
}

// TestPullDirectory_SamePack_CaseInsensitive_PassesAuthorize confirms that the
// same-pack (case-insensitive) path does not produce the cross-pack error from
// authorizeDirectoryProfileDependency. With nil db the call proceeds past the
// authority check and fails at LoadActiveProfile (nil-db panic or error) — that
// is the expected outcome showing the guard was passed.
func TestPullDirectory_SamePack_CaseInsensitive_PassesAuthorize(t *testing.T) {
	adapter := &directoryReaderAdapter{db: nil, engine: nil}

	var crossPackErrSeen bool
	func() {
		defer func() { _ = recover() }()
		_, err := adapter.PullDirectory(context.Background(), DirectoryPullRequest{
			RequesterOwnerPack: "Pack-A",
			ProfileOwnerPack:   "pack-a",
			ProfileName:        "some-profile",
			ObjectKey:          "contacts",
			ItemsPath:          "items",
		})
		if err != nil && containsSubstr(err.Error(), "db is required for cross-pack") {
			crossPackErrSeen = true
		}
	}()
	if crossPackErrSeen {
		t.Error("same-pack (case-insensitive) must not trigger cross-pack denial")
	}
}

// ---------------------------------------------------------------------------
// accumulateDirectoryPages — pure-function tests (no Engine required)
// ---------------------------------------------------------------------------

func TestAccumulateDirectoryPages_StalledPage_ReturnsError(t *testing.T) {
	// hasMore=true but zero items on first page — must fail closed.
	calls := 0
	next := func() ([]byte, bool, error) {
		calls++
		return []byte(`[]`), true, nil
	}
	_, err := accumulateDirectoryPages("", 0, next)
	if err == nil {
		t.Fatal("expected stall error, got nil")
	}
	if !containsSubstr(err.Error(), "stalled") {
		t.Errorf("expected 'stalled' in error, got: %q", err.Error())
	}
	if calls != 1 {
		t.Errorf("expected exactly 1 page call before stall detection, got %d", calls)
	}
}

func TestAccumulateDirectoryPages_TwoPagesHappyPath(t *testing.T) {
	// Two pages: page 1 has 2 items + hasMore=true, page 2 has 1 item + hasMore=false.
	call := 0
	pages := []struct {
		body    []byte
		hasMore bool
	}{
		{[]byte(`[{"id":"1"},{"id":"2"}]`), true},
		{[]byte(`[{"id":"3"}]`), false},
	}
	next := func() ([]byte, bool, error) {
		p := pages[call]
		call++
		return p.body, p.hasMore, nil
	}
	items, err := accumulateDirectoryPages("", 0, next)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(items) != 3 {
		t.Fatalf("expected 3 accumulated items, got %d", len(items))
	}
}

func TestAccumulateDirectoryPages_ExceedsMaxItems_ReturnsError(t *testing.T) {
	// maxItems=1; page returns 2 items.
	next := func() ([]byte, bool, error) {
		return []byte(`[{"id":"1"},{"id":"2"}]`), false, nil
	}
	_, err := accumulateDirectoryPages("", 1, next)
	if err == nil {
		t.Fatal("expected maxItems error, got nil")
	}
	if !containsSubstr(err.Error(), "MaxItems") {
		t.Errorf("expected 'MaxItems' in error, got: %q", err.Error())
	}
}

func TestAccumulateDirectoryPages_SinglePageNoMore_ReturnsItems(t *testing.T) {
	next := func() ([]byte, bool, error) {
		return []byte(`[{"id":"a"},{"id":"b"}]`), false, nil
	}
	items, err := accumulateDirectoryPages("", 0, next)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(items) != 2 {
		t.Fatalf("expected 2 items, got %d", len(items))
	}
}

// containsSubstr is a tiny helper to avoid importing strings in test helpers.
func containsSubstr(s, sub string) bool {
	return len(s) >= len(sub) && (s == sub || len(sub) == 0 ||
		func() bool {
			for i := 0; i <= len(s)-len(sub); i++ {
				if s[i:i+len(sub)] == sub {
					return true
				}
			}
			return false
		}())
}
