package main

import (
	"bytes"
	"context"
	"strings"
	"testing"
)

func TestObsoleteFlowsNonEmpty(t *testing.T) {
	if len(obsoleteFlows) == 0 {
		t.Fatal("obsoleteFlows must not be empty — Phase 0 requires ~16 flows to deactivate")
	}
	// Design doc says "~16"; actual enumerated list is 19 (BN_* and
	// Create/Update shorthands expanded). Sanity-check the range.
	if len(obsoleteFlows) < 15 || len(obsoleteFlows) > 25 {
		t.Errorf("obsoleteFlows length = %d, expected ~16-20", len(obsoleteFlows))
	}
}

func TestObsoleteFlowsUnique(t *testing.T) {
	seen := make(map[string]struct{}, len(obsoleteFlows))
	for _, f := range obsoleteFlows {
		if f == "" {
			t.Errorf("empty DeveloperName in obsoleteFlows")
		}
		if _, dup := seen[f]; dup {
			t.Errorf("duplicate DeveloperName in obsoleteFlows: %q", f)
		}
		seen[f] = struct{}{}
	}
}

func TestBuildStatementsEmitsTwoPerFlow(t *testing.T) {
	flows := []string{"Foo_Flow", "Bar_Flow"}
	stmts := buildStatements(flows)
	if got, want := len(stmts), 4; got != want {
		t.Fatalf("len(stmts) = %d, want %d (2 per flow)", got, want)
	}
}

func TestBuildStatementsContainsIdempotentGuards(t *testing.T) {
	stmts := buildStatements([]string{"Foo_Flow"})
	if len(stmts) != 2 {
		t.Fatalf("expected 2 statements, got %d", len(stmts))
	}

	clear := stmts[0]
	if !strings.Contains(clear.sql, "UPDATE FlowDefinition") {
		t.Errorf("stmt[0] missing FlowDefinition UPDATE: %s", clear.sql)
	}
	if !strings.Contains(clear.sql, "ActiveVersionId = ''") {
		t.Errorf("stmt[0] does not clear ActiveVersionId: %s", clear.sql)
	}
	if !strings.Contains(clear.sql, "ActiveVersionId IS NOT NULL AND ActiveVersionId <> ''") {
		t.Errorf("stmt[0] missing idempotency guard: %s", clear.sql)
	}
	if !strings.Contains(clear.sql, "DeveloperName = ?") {
		t.Errorf("stmt[0] must use parameter placeholder, not literal: %s", clear.sql)
	}

	obsolete := stmts[1]
	if !strings.Contains(obsolete.sql, "UPDATE flow") {
		t.Errorf("stmt[1] missing flow UPDATE: %s", obsolete.sql)
	}
	if !strings.Contains(obsolete.sql, "Status = 'Obsolete'") {
		t.Errorf("stmt[1] does not set Obsolete: %s", obsolete.sql)
	}
	if !strings.Contains(obsolete.sql, "f.Status <> 'Obsolete'") {
		t.Errorf("stmt[1] missing idempotency guard: %s", obsolete.sql)
	}
	if !strings.Contains(obsolete.sql, "JOIN FlowDefinition") {
		t.Errorf("stmt[1] must join FlowDefinition to resolve DeveloperName: %s", obsolete.sql)
	}

	for i, s := range stmts {
		if len(s.args) != 1 {
			t.Errorf("stmt[%d] args = %v, want exactly 1", i, s.args)
			continue
		}
		if s.args[0] != "Foo_Flow" {
			t.Errorf("stmt[%d] args[0] = %v, want Foo_Flow", i, s.args[0])
		}
	}
}

func TestDryRunPrintsSQLAndDoesNotConnect(t *testing.T) {
	var buf bytes.Buffer
	flows := []string{"Alpha_Flow", "Beta_Flow"}

	// apply=false with no env vars set — must succeed (no DB access).
	if err := run(context.Background(), false, false, &buf, flows); err != nil {
		t.Fatalf("dry-run failed: %v", err)
	}

	out := buf.String()
	if !strings.Contains(out, "DRY RUN") {
		t.Errorf("dry-run output missing DRY RUN banner: %s", out)
	}
	if !strings.Contains(out, "Alpha_Flow") {
		t.Errorf("dry-run output missing Alpha_Flow: %s", out)
	}
	if !strings.Contains(out, "Beta_Flow") {
		t.Errorf("dry-run output missing Beta_Flow: %s", out)
	}
	if !strings.Contains(out, "UPDATE FlowDefinition") {
		t.Errorf("dry-run missing FlowDefinition UPDATE: %s", out)
	}
	if !strings.Contains(out, "UPDATE flow") {
		t.Errorf("dry-run missing flow UPDATE: %s", out)
	}
}

func TestRunRejectsEmptyFlowList(t *testing.T) {
	var buf bytes.Buffer
	if err := run(context.Background(), false, false, &buf, nil); err == nil {
		t.Fatal("expected error on empty flow list")
	}
}

func TestCheckTestDBGate_AllowsTestPrefix(t *testing.T) {
	t.Setenv("TIDB_USER", testDBUserPrefix+".root")
	if err := checkTestDBGate(false); err != nil {
		t.Fatalf("expected test-DB user to be allowed, got: %v", err)
	}
}

func TestCheckTestDBGate_RejectsNonTestPrefix(t *testing.T) {
	// Use a user that explicitly does not start with the test prefix
	// (staging user from the task description).
	t.Setenv("TIDB_USER", "2JuXyXHYxHrLAUr.root")
	err := checkTestDBGate(false)
	if err == nil {
		t.Fatal("expected gate to reject non-test TIDB_USER without escape hatch")
	}
	if !strings.Contains(err.Error(), "refusing to --apply") {
		t.Errorf("error message should explain the refusal, got: %v", err)
	}
}

func TestCheckTestDBGate_RejectsEmptyUser(t *testing.T) {
	t.Setenv("TIDB_USER", "")
	if err := checkTestDBGate(false); err == nil {
		t.Fatal("expected gate to reject empty TIDB_USER without escape hatch")
	}
}

func TestCheckTestDBGate_EscapeHatchAllowsNonTest(t *testing.T) {
	t.Setenv("TIDB_USER", "3C9pXLbjDaTBomw.root") // prod prefix
	if err := checkTestDBGate(true); err != nil {
		t.Fatalf("expected escape hatch to allow non-test user, got: %v", err)
	}
}

func TestApplyPathRefusesNonTestDB(t *testing.T) {
	// Full end-to-end exercise of run() with apply=true — must be refused
	// by the gate before any DB access is attempted. TIDB_USER is set to
	// the known staging prefix; no TLS, no DB creds needed, because the
	// gate short-circuits before openDB.
	t.Setenv("TIDB_USER", "2JuXyXHYxHrLAUr.root")
	t.Setenv("TIDB_HOST", "gateway01.eu-central-1.prod.aws.tidbcloud.com")
	t.Setenv("TIDB_PASSWORD", "irrelevant")
	t.Setenv("TIDB_DATABASE", "nexuscrm")

	var buf bytes.Buffer
	err := run(context.Background(), true, false, &buf, []string{"Foo_Flow"})
	if err == nil {
		t.Fatal("expected run() to refuse apply against non-test user")
	}
	if !strings.Contains(err.Error(), "refusing to --apply") {
		t.Errorf("error should be from the test-DB gate, got: %v", err)
	}
}
