package services

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"strings"
	"testing"

	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"
)

// openTestPool returns a sqlx.DB connected via TEST_DATABASE_URL.
// Returns nil + skips the test when the env var is unset, mirroring
// the pattern used by sibling live-DB tests in this package.
func openTestPool(t *testing.T) *sqlx.DB {
	t.Helper()
	url := os.Getenv("TEST_DATABASE_URL")
	if url == "" {
		return nil
	}
	pool, err := sqlx.Connect("postgres", url)
	if err != nil {
		t.Fatalf("connect: %v", err)
	}
	return pool
}

// TestValidateConditionExprShapes covers the grammar shapes (leaf,
// composite, nested composite) and the rejection paths. The catalog
// lookup is exercised via the live DB in TestValidateConditionExpr_Live18
// below; here we use json-only shape checks to keep the unit tests
// independent of database availability.
func TestValidateConditionExprShapes(t *testing.T) {
	// Bypass the DB-backed flag-existence check by passing nil db with
	// an expression that has no leaves once unmarshalled. Since the
	// grammar walker rejects empty/invalid shapes BEFORE the DB lookup,
	// shape-only assertions work without a pool. For the leaf-flag
	// existence check we'd need a fixture DB — that's the live test.
	ctx := context.Background()

	cases := []struct {
		name      string
		input     string
		wantError string // empty = success-path placeholder
		wantInvalid bool
	}{
		{name: "empty input", input: ``, wantInvalid: false},
		{name: "JSON null", input: `null`, wantInvalid: false},
		{name: "bad JSON", input: `{flag:`, wantInvalid: true, wantError: "valid JSON"},
		{name: "leaf with empty flag", input: `{"flag":""}`, wantInvalid: true, wantError: "empty flag"},
		{name: "leaf AND op", input: `{"flag":"x","op":"and"}`, wantInvalid: true, wantError: "mutually exclusive"},
		{name: "neither flag nor op", input: `{}`, wantInvalid: true, wantError: "must carry either"},
		{name: "bad op", input: `{"op":"xor","args":[{"flag":"x"}]}`, wantInvalid: true, wantError: "must be 'and' or 'or'"},
		{name: "empty args", input: `{"op":"and","args":[]}`, wantInvalid: true, wantError: "empty args"},
		{name: "nested bad shape", input: `{"op":"and","args":[{"flag":"x"},{"flag":""}]}`, wantInvalid: true, wantError: "empty flag"},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			err := ValidateConditionExpr(ctx, nil, json.RawMessage(c.input))
			if c.wantInvalid {
				if err == nil {
					t.Fatalf("expected error, got nil")
				}
				if !errors.Is(err, ErrInvalidInput) {
					t.Errorf("error %v is not ErrInvalidInput", err)
				}
				if c.wantError != "" && !strings.Contains(err.Error(), c.wantError) {
					t.Errorf("error %q missing substring %q", err.Error(), c.wantError)
				}
				return
			}
			// success-path: empty/null inputs go through without an err.
			// Anything else hits the DB lookup with nil pool → nil-deref;
			// that path is covered by the live test below.
			if err != nil {
				t.Fatalf("expected no error for %q, got %v", c.input, err)
			}
		})
	}
}

// TestValidateConditionExpr_LiveCatalog runs the validator against the
// real paliad.scenario_flag_catalog (the 3 seeded flags) using a sample
// of each grammar shape. Skips when DATABASE_URL isn't set.
func TestValidateConditionExpr_LiveCatalog(t *testing.T) {
	pool := openTestPool(t)
	if pool == nil {
		t.Skip("DATABASE_URL not set — skipping live-catalog validation")
	}
	ctx := context.Background()

	good := []string{
		`{"flag":"with_ccr"}`,
		`{"flag":"with_amend"}`,
		`{"flag":"with_cci"}`,
		`{"op":"and","args":[{"flag":"with_ccr"},{"flag":"with_amend"}]}`,
		`{"op":"or","args":[{"flag":"with_ccr"},{"flag":"with_cci"}]}`,
		`{"op":"and","args":[{"flag":"with_ccr"},{"op":"or","args":[{"flag":"with_amend"},{"flag":"with_cci"}]}]}`,
	}
	for _, g := range good {
		if err := ValidateConditionExpr(ctx, pool, json.RawMessage(g)); err != nil {
			t.Errorf("expected %q to validate, got %v", g, err)
		}
	}

	bad := []struct{ in, contains string }{
		{`{"flag":"with_nonsense"}`, "unknown flag"},
		{`{"op":"and","args":[{"flag":"with_ccr"},{"flag":"never_seen"}]}`, "unknown flag"},
	}
	for _, b := range bad {
		err := ValidateConditionExpr(ctx, pool, json.RawMessage(b.in))
		if err == nil {
			t.Errorf("expected %q to fail validation", b.in)
			continue
		}
		if !strings.Contains(err.Error(), b.contains) {
			t.Errorf("error %q for %q missing substring %q", err.Error(), b.in, b.contains)
		}
	}
}

// TestConditionExpr_AllLiveRowsValidate exercises the validator on every
// row currently in paliad.sequencing_rules. Per design §4.1: "all 18
// existing rows must validate" — this test enforces the invariant on
// every deploy so a new editorial entry that breaks the grammar fails
// CI before it lands.
func TestConditionExpr_AllLiveRowsValidate(t *testing.T) {
	pool := openTestPool(t)
	if pool == nil {
		t.Skip("DATABASE_URL not set — skipping live-rows test")
	}
	ctx := context.Background()

	rows, err := pool.QueryContext(ctx,
		`SELECT id, condition_expr::text
		   FROM paliad.sequencing_rules
		  WHERE condition_expr IS NOT NULL
		    AND is_active = true
		    AND lifecycle_state = 'published'`)
	if err != nil {
		t.Fatalf("load condition_expr rows: %v", err)
	}
	defer rows.Close()
	count := 0
	for rows.Next() {
		var id, expr string
		if err := rows.Scan(&id, &expr); err != nil {
			t.Fatalf("scan: %v", err)
		}
		count++
		if err := ValidateConditionExpr(ctx, pool, json.RawMessage(expr)); err != nil {
			t.Errorf("rule %s carries non-conforming condition_expr %s: %v", id, expr, err)
		}
	}
	if err := rows.Err(); err != nil {
		t.Fatalf("rows err: %v", err)
	}
	if count == 0 {
		t.Skip("no condition_expr rows in DB — nothing to validate")
	}
	t.Logf("validated %d live condition_expr rows", count)
}
