package workbenchruntime

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestRealtimeSyncTxOptionsAreReadOnlyRepeatableRead(t *testing.T) {
	options := realtimeSyncTxOptions()
	if options.Isolation != sql.LevelRepeatableRead {
		t.Fatalf("isolation=%v, want repeatable read", options.Isolation)
	}
	if !options.ReadOnly {
		t.Fatal("realtime sync transaction must be read only")
	}
}

func TestNormalizeRealtimeSyncQueryRequiresScopedActorAndBoundsLimit(t *testing.T) {
	query, validation := normalizeRealtimeSyncQuery(realtimeSyncQuery{
		SessionID: "  ses_sync  ",
		Limit:     900,
		Actor:     sessionActor{ID: " usr_owner ", Role: " USER "},
	})
	if validation != nil {
		t.Fatalf("unexpected validation: %#v", validation)
	}
	if query.SessionID != "ses_sync" || query.Actor.ID != "usr_owner" || query.Actor.Role != "user" {
		t.Fatalf("query was not normalized: %#v", query)
	}
	if query.Limit != realtimeSyncMaxLimit {
		t.Fatalf("limit=%d, want %d", query.Limit, realtimeSyncMaxLimit)
	}

	_, validation = normalizeRealtimeSyncQuery(realtimeSyncQuery{TraceID: "trc_sync", Actor: sessionActor{Role: "user"}})
	if validation == nil || validation.Status != http.StatusForbidden || validation.Code != "workbench_sync_actor_required" {
		t.Fatalf("ordinary user without owner id validation=%#v", validation)
	}

	admin, validation := normalizeRealtimeSyncQuery(realtimeSyncQuery{TraceID: "trc_sync", Actor: sessionActor{Role: "admin"}})
	if validation != nil || admin.Actor.ID != "" {
		t.Fatalf("admin should not require owner id: query=%#v validation=%#v", admin, validation)
	}

	_, validation = normalizeRealtimeSyncQuery(realtimeSyncQuery{Actor: sessionActor{ID: "usr_owner", Role: "user"}})
	if validation == nil || validation.Code != "workbench_sync_scope_required" {
		t.Fatalf("missing scope validation=%#v", validation)
	}

	_, validation = normalizeRealtimeSyncQuery(realtimeSyncQuery{SessionID: "ses_sync", AfterOutboxSeq: -1, Actor: sessionActor{ID: "usr_owner", Role: "user"}})
	if validation == nil || validation.Code != "workbench_sync_cursor_invalid" {
		t.Fatalf("negative cursor validation=%#v", validation)
	}

	_, validation = normalizeRealtimeSyncQuery(realtimeSyncQuery{SessionID: "ses_sync", SnapshotOnly: true, DeltaOnly: true, Actor: sessionActor{ID: "usr_owner", Role: "user"}})
	if validation == nil || validation.Code != "workbench_sync_mode_invalid" {
		t.Fatalf("conflicting sync mode validation=%#v", validation)
	}
}

func TestRealtimeSyncRouteIsPostOnlyAndRejectsUnknownFields(t *testing.T) {
	server := &Server{config: Config{}, mux: http.NewServeMux()}
	server.routes()

	methodRecorder := httptest.NewRecorder()
	server.mux.ServeHTTP(methodRecorder, httptest.NewRequest(http.MethodGet, "/v1/workbench-runtime/sync", nil))
	if methodRecorder.Code != http.StatusMethodNotAllowed {
		t.Fatalf("GET status=%d, want %d", methodRecorder.Code, http.StatusMethodNotAllowed)
	}
	if methodRecorder.Header().Get("Allow") != "POST" {
		t.Fatalf("Allow=%q, want POST", methodRecorder.Header().Get("Allow"))
	}

	unknownRecorder := httptest.NewRecorder()
	unknownRequest := httptest.NewRequest(http.MethodPost, "/v1/workbench-runtime/sync", strings.NewReader(`{"sessionId":"ses_sync","actor":{"id":"usr_owner","role":"user"},"unknown":true}`))
	server.mux.ServeHTTP(unknownRecorder, unknownRequest)
	if unknownRecorder.Code != http.StatusBadRequest || !strings.Contains(unknownRecorder.Body.String(), "invalid_json") {
		t.Fatalf("unknown field response=%d %s", unknownRecorder.Code, unknownRecorder.Body.String())
	}

	trailingRecorder := httptest.NewRecorder()
	trailingRequest := httptest.NewRequest(http.MethodPost, "/v1/workbench-runtime/sync", strings.NewReader(`{"sessionId":"ses_sync","actor":{"id":"usr_owner","role":"user"}} {}`))
	server.mux.ServeHTTP(trailingRecorder, trailingRequest)
	if trailingRecorder.Code != http.StatusBadRequest || !strings.Contains(trailingRecorder.Body.String(), "invalid_json") {
		t.Fatalf("trailing JSON response=%d %s", trailingRecorder.Code, trailingRecorder.Body.String())
	}
}

func TestReadRealtimeSyncSnapshotUsesOwnerScopeAndSingleCutoff(t *testing.T) {
	queryer := newRealtimeSyncFakeQueryer("usr_owner")
	query := realtimeSyncQuery{
		SessionID:      "ses_sync",
		TraceID:        "trc_sync",
		AfterOutboxSeq: 0,
		Limit:          2,
		Actor:          sessionActor{ID: "usr_owner", Role: "user"},
	}
	snapshot, err := readRealtimeSyncSnapshot(context.Background(), queryer, query)
	if err != nil {
		t.Fatalf("read sync snapshot: %v", err)
	}
	if snapshot.CutoffOutboxSeq != 3 || snapshot.CursorOutboxSeq != 2 || !snapshot.HasMore {
		t.Fatalf("unexpected cursor boundary: %#v", snapshot)
	}
	if len(snapshot.Events) != 2 {
		t.Fatalf("events=%d, want 2", len(snapshot.Events))
	}
	first := snapshot.Events[0]
	if first.OutboxEventID != "obe_1" || first.EntityFamily != "traceEvents" || first.EntityID != "evt_1" {
		t.Fatalf("event identity fields were not returned: %#v", first)
	}
	if first.EventSeq != int64(101) || first.AggregateID != "trc_sync" || first.AggregateSeq != 1 || first.ProjectionRevision != 11 {
		t.Fatalf("event aggregate fields were not returned: %#v", first)
	}
	if len(snapshot.Facts) != len(realtimeSyncFactSpecs) {
		t.Fatalf("fact families=%d, want %d: %#v", len(snapshot.Facts), len(realtimeSyncFactSpecs), snapshot.Facts)
	}
	for _, spec := range realtimeSyncFactSpecs {
		if len(snapshot.Facts[spec.Family]) != 1 {
			t.Fatalf("facts[%s]=%#v, want one fact", spec.Family, snapshot.Facts[spec.Family])
		}
	}
	if len(queryer.calls) != 3+len(realtimeSyncFactSpecs) {
		t.Fatalf("query calls=%v", queryer.calls)
	}
	if queryer.calls[0].kind != "scope" || queryer.calls[1].kind != "cutoff" || queryer.calls[2].kind != "events" {
		t.Fatalf("snapshot boundary query order=%v", queryer.calls)
	}
	if got := queryer.calls[0].args; len(got) != 4 || got[2] != false || got[3] != "usr_owner" {
		t.Fatalf("owner scope args=%#v", got)
	}
	if got := queryer.calls[2].args; len(got) != 5 || got[3] != int64(3) || got[4] != 3 {
		t.Fatalf("events must use cutoff and limit+1: %#v", got)
	}
}

func TestReadRealtimeSyncSnapshotRejectsOtherOwnerBeforeReadingFacts(t *testing.T) {
	if !strings.Contains(realtimeSyncResolveScopeSQL, "sessions.owner_user_id = $4") {
		t.Fatalf("ordinary user scope must be constrained by workbench_sessions.owner_user_id: %s", realtimeSyncResolveScopeSQL)
	}
	queryer := newRealtimeSyncFakeQueryer("usr_owner")
	_, err := readRealtimeSyncSnapshot(context.Background(), queryer, realtimeSyncQuery{
		SessionID: "ses_sync",
		Limit:     100,
		Actor:     sessionActor{ID: "usr_other", Role: "user"},
	})
	if !errors.Is(err, sql.ErrNoRows) {
		t.Fatalf("error=%v, want sql.ErrNoRows", err)
	}
	if len(queryer.calls) != 1 || queryer.calls[0].kind != "scope" {
		t.Fatalf("unauthorized scope must stop before outbox/facts reads: %v", queryer.calls)
	}
}

func TestReadRealtimeSyncSnapshotOnlySkipsHistoryAndReadsBoundedCurrentFacts(t *testing.T) {
	queryer := newRealtimeSyncFakeQueryer("usr_owner")
	snapshot, err := readRealtimeSyncSnapshot(context.Background(), queryer, realtimeSyncQuery{
		SessionID:    "ses_sync",
		Limit:        100,
		SnapshotOnly: true,
		Actor:        sessionActor{ID: "usr_owner", Role: "user"},
	})
	if err != nil {
		t.Fatalf("read snapshot-only sync: %v", err)
	}
	if len(snapshot.Events) != 0 || snapshot.HasMore || snapshot.CursorOutboxSeq != snapshot.CutoffOutboxSeq {
		t.Fatalf("snapshot-only boundary=%#v", snapshot)
	}
	if len(snapshot.Facts) != len(realtimeSyncSnapshotFactSpecs) {
		t.Fatalf("snapshot-only facts=%#v", snapshot.Facts)
	}
	for _, call := range queryer.calls {
		if call.kind == "events" || call.kind == "facts.parts" || call.kind == "facts.traceEvents" || call.kind == "facts.checkpoints" {
			t.Fatalf("snapshot-only performed unbounded read: %v", queryer.calls)
		}
	}
}

func TestReadRealtimeSyncDeltaOnlyReadsOutboxWithoutFacts(t *testing.T) {
	queryer := newRealtimeSyncFakeQueryer("usr_owner")
	snapshot, err := readRealtimeSyncSnapshot(context.Background(), queryer, realtimeSyncQuery{
		SessionID:      "ses_sync",
		AfterOutboxSeq: 1,
		Limit:          100,
		DeltaOnly:      true,
		Actor:          sessionActor{ID: "usr_owner", Role: "user"},
	})
	if err != nil {
		t.Fatalf("read delta-only sync: %v", err)
	}
	if len(snapshot.Events) == 0 || len(snapshot.Facts) != 0 {
		t.Fatalf("delta-only result=%#v", snapshot)
	}
	for _, call := range queryer.calls {
		if strings.HasPrefix(call.kind, "facts.") {
			t.Fatalf("delta-only read facts: %v", queryer.calls)
		}
	}
}

func TestReadRealtimeSyncSnapshotClampsForeignCursorToScopedCutoff(t *testing.T) {
	queryer := newRealtimeSyncFakeQueryer("usr_owner")
	queryer.events = [][]any{}
	snapshot, err := readRealtimeSyncSnapshot(context.Background(), queryer, realtimeSyncQuery{
		SessionID:      "ses_sync",
		AfterOutboxSeq: 999,
		Limit:          100,
		Actor:          sessionActor{ID: "usr_owner", Role: "user"},
	})
	if err != nil {
		t.Fatalf("read sync snapshot: %v", err)
	}
	if snapshot.CutoffOutboxSeq != 3 || snapshot.CursorOutboxSeq != 3 || snapshot.HasMore || len(snapshot.Events) != 0 {
		t.Fatalf("foreign cursor was not clamped to scoped cutoff: %#v", snapshot)
	}
}

func TestReadRealtimeSyncSnapshotAllowsAdminWithoutOwnerFilter(t *testing.T) {
	queryer := newRealtimeSyncFakeQueryer("usr_owner")
	snapshot, err := readRealtimeSyncSnapshot(context.Background(), queryer, realtimeSyncQuery{
		TraceID: "trc_sync",
		Limit:   3,
		Actor:   sessionActor{Role: "admin"},
	})
	if err != nil {
		t.Fatalf("admin sync: %v", err)
	}
	if len(queryer.calls) == 0 || queryer.calls[0].args[2] != true || queryer.calls[0].args[3] != "" {
		t.Fatalf("admin scope args=%#v", queryer.calls)
	}
	if snapshot.CursorOutboxSeq != snapshot.CutoffOutboxSeq || snapshot.HasMore {
		t.Fatalf("admin complete page boundary=%#v", snapshot)
	}
}

type realtimeSyncFakeCall struct {
	kind  string
	query string
	args  []any
}

type realtimeSyncFakeQueryer struct {
	ownerID   string
	sessionID string
	cutoff    int64
	events    [][]any
	facts     map[string][][]any
	calls     []realtimeSyncFakeCall
}

func newRealtimeSyncFakeQueryer(ownerID string) *realtimeSyncFakeQueryer {
	facts := map[string][][]any{}
	for _, spec := range realtimeSyncFactSpecs {
		facts[spec.Table] = [][]any{{fmt.Sprintf(`{"family":%q,"sessionId":"ses_sync"}`, spec.Family)}}
	}
	return &realtimeSyncFakeQueryer{
		ownerID:   ownerID,
		sessionID: "ses_sync",
		cutoff:    3,
		events:    [][]any{realtimeSyncTestEventRow(1), realtimeSyncTestEventRow(2), realtimeSyncTestEventRow(3)},
		facts:     facts,
	}
}

func (queryer *realtimeSyncFakeQueryer) QueryRowContext(_ context.Context, query string, args ...any) realtimeSyncRow {
	if query == realtimeSyncResolveScopeSQL {
		queryer.record("scope", query, args)
		isAdmin, _ := args[2].(bool)
		actorID, _ := args[3].(string)
		if !isAdmin && actorID != queryer.ownerID {
			return realtimeSyncFakeRow{err: sql.ErrNoRows}
		}
		return realtimeSyncFakeRow{values: []any{queryer.sessionID}}
	}
	if query == realtimeSyncCutoffSQL {
		queryer.record("cutoff", query, args)
		return realtimeSyncFakeRow{values: []any{queryer.cutoff}}
	}
	return realtimeSyncFakeRow{err: fmt.Errorf("unexpected QueryRowContext: %s", query)}
}

func (queryer *realtimeSyncFakeQueryer) QueryContext(_ context.Context, query string, args ...any) (realtimeSyncRows, error) {
	if query == realtimeSyncEventsSQL {
		queryer.record("events", query, args)
		return &realtimeSyncFakeRows{values: queryer.events}, nil
	}
	for _, spec := range realtimeSyncFactSpecs {
		if strings.Contains(query, "FROM "+spec.Table+" ") {
			queryer.record("facts."+spec.Family, query, args)
			return &realtimeSyncFakeRows{values: queryer.facts[spec.Table]}, nil
		}
	}
	return nil, fmt.Errorf("unexpected QueryContext: %s", query)
}

func (queryer *realtimeSyncFakeQueryer) record(kind string, query string, args []any) {
	cloned := append([]any(nil), args...)
	queryer.calls = append(queryer.calls, realtimeSyncFakeCall{kind: kind, query: query, args: cloned})
}

type realtimeSyncFakeRow struct {
	values []any
	err    error
}

func (row realtimeSyncFakeRow) Scan(destinations ...any) error {
	if row.err != nil {
		return row.err
	}
	return assignRealtimeSyncFakeValues(destinations, row.values)
}

type realtimeSyncFakeRows struct {
	values [][]any
	index  int
	err    error
}

func (rows *realtimeSyncFakeRows) Close() error { return nil }

func (rows *realtimeSyncFakeRows) Err() error { return rows.err }

func (rows *realtimeSyncFakeRows) Next() bool { return rows.index < len(rows.values) }

func (rows *realtimeSyncFakeRows) Scan(destinations ...any) error {
	if rows.index >= len(rows.values) {
		return errors.New("realtime sync fake rows exhausted")
	}
	values := rows.values[rows.index]
	rows.index++
	return assignRealtimeSyncFakeValues(destinations, values)
}

func assignRealtimeSyncFakeValues(destinations []any, values []any) error {
	if len(destinations) != len(values) {
		return fmt.Errorf("scan destinations=%d values=%d", len(destinations), len(values))
	}
	for index, destination := range destinations {
		value := values[index]
		switch target := destination.(type) {
		case *string:
			if value == nil {
				return fmt.Errorf("cannot scan nil into string")
			}
			*target = fmt.Sprint(value)
		case *int64:
			integer, ok := value.(int64)
			if !ok {
				return fmt.Errorf("cannot scan %T into int64", value)
			}
			*target = integer
		case *sql.NullString:
			if value == nil {
				*target = sql.NullString{}
			} else {
				*target = sql.NullString{String: fmt.Sprint(value), Valid: true}
			}
		case *sql.NullInt64:
			if value == nil {
				*target = sql.NullInt64{}
			} else if integer, ok := value.(int64); ok {
				*target = sql.NullInt64{Int64: integer, Valid: true}
			} else {
				return fmt.Errorf("cannot scan %T into NullInt64", value)
			}
		case *sql.NullBool:
			if value == nil {
				*target = sql.NullBool{}
			} else if boolean, ok := value.(bool); ok {
				*target = sql.NullBool{Bool: boolean, Valid: true}
			} else {
				return fmt.Errorf("cannot scan %T into NullBool", value)
			}
		default:
			return fmt.Errorf("unsupported scan destination %T", destination)
		}
	}
	return nil
}

func realtimeSyncTestEventRow(sequence int64) []any {
	return []any{
		sequence,
		fmt.Sprintf("obe_%d", sequence),
		"traceEvents",
		fmt.Sprintf("evt_%d", sequence),
		int64(100) + sequence,
		"trc_sync",
		sequence,
		int64(10) + sequence,
		"trc_sync",
		"ses_sync",
		"turn_sync",
		nil,
		sequence,
		sequence,
		fmt.Sprintf("source_%d", sequence),
		"event",
		false,
		false,
		fmt.Sprintf(`{"sequence":%d}`, sequence),
		"2026-07-10T12:00:00Z",
	}
}
