package postgres

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/hex"
	"testing"
	"time"

	"github.com/bridge/ai-customer-service/internal/domain/audit"
	"github.com/bridge/ai-customer-service/internal/domain/session"
	"github.com/bridge/ai-customer-service/internal/domain/ticket"
)

func getDSN() string {
	return "host=localhost port=5434 user=ai_cs password=ai_cs_secret dbname=ai_customer_service sslmode=disable"
}

func uniqueID(prefix string) string {
	b := make([]byte, 16)
	rand.Read(b)
	b[6] = (b[6] & 0x0f) | 0x40
	b[8] = (b[8] & 0x3f) | 0x80
	uuid := hex.EncodeToString(b)
	return uuid[:8] + "-" + uuid[8:12] + "-" + uuid[12:16] + "-" + uuid[16:20] + "-" + uuid[20:]
}

func openDBForTest(t *testing.T) *sql.DB {
	dsn := getDSN()
	if dsn == "" {
		t.Skip("AI_CS_POSTGRES_DSN not set")
	}
	db, err := Open(Config{
		DSN:             dsn,
		MaxOpenConns:    5,
		MaxIdleConns:    2,
		ConnMaxLifetime: time.Second * 30,
	})
	if err != nil {
		t.Fatalf("failed to open DB: %v", err)
	}
	return db
}

// --- TicketStore tests ---

func TestTicketStore_CreateAndGet(t *testing.T) {
	db := openDBForTest(t)
	defer db.Close()

	sessionStore := NewSessionStore(db)
	ticketStore := NewTicketStore(db)
	ctx := context.Background()
	now := time.Now().Truncate(time.Second)

	// Create session first (FK constraint)
	sess, err := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
	if err != nil {
		t.Fatalf("failed to create session: %v", err)
	}

	tkt := &ticket.Ticket{
		ID:              uniqueID("tick"),
		SessionID:       sess.ID,
		UserID:          "user-001",
		Priority:        ticket.PriorityP1,
		Status:          ticket.StatusOpen,
		HandoffReason:   "Test handoff",
		AssignedTo:      "agent-001",
		ContextSnapshot: map[string]any{"key": "value"},
		CreatedAt:       now,
		UpdatedAt:        now,
	}

	if err := ticketStore.Create(ctx, tkt); err != nil {
		t.Fatalf("Create failed: %v", err)
	}

	fetched, err := ticketStore.GetByID(ctx, tkt.ID)
	if err != nil {
		t.Fatalf("GetByID failed: %v", err)
	}
	if fetched.ID != tkt.ID {
		t.Errorf("expected ID %s, got %s", tkt.ID, fetched.ID)
	}
	if fetched.SessionID != tkt.SessionID {
		t.Errorf("expected SessionID %s, got %s", tkt.SessionID, fetched.SessionID)
	}
	if fetched.Priority != ticket.PriorityP1 {
		t.Errorf("expected Priority P1, got %s", fetched.Priority)
	}
	if fetched.Status != ticket.StatusOpen {
		t.Errorf("expected Status open, got %s", fetched.Status)
	}
}

func TestTicketStore_GetStats(t *testing.T) {
	db := openDBForTest(t)
	defer db.Close()

	store := NewTicketStore(db)
	ctx := context.Background()

	stats, err := store.GetStats(ctx)
	if err != nil {
		t.Fatalf("GetStats failed: %v", err)
	}

	if stats.Total < 0 {
		t.Errorf("expected non-negative Total, got %d", stats.Total)
	}
	if stats.ByChannel == nil {
		t.Error("expected non-nil ByChannel")
	}
	if stats.ByPriority == nil {
		t.Error("expected non-nil ByPriority")
	}
}

func TestTicketStore_Create_NilTicket(t *testing.T) {
	store := NewTicketStore(nil)
	err := store.Create(context.Background(), nil)
	if err == nil {
		t.Error("expected error for nil ticket")
	}
}

func TestTicketStore_Create_NilDB(t *testing.T) {
	store := NewTicketStore(nil)
	err := store.Create(context.Background(), &ticket.Ticket{})
	if err == nil {
		t.Error("expected error for nil db")
	}
}

func TestTicketStore_GetByID_NilDB(t *testing.T) {
	store := NewTicketStore(nil)
	_, err := store.GetByID(context.Background(), "any-id")
	if err == nil {
		t.Error("expected error for nil db")
	}
}

func TestTicketStore_GetStats_NilDB(t *testing.T) {
	store := NewTicketStore(nil)
	_, err := store.GetStats(context.Background())
	if err == nil {
		t.Error("expected error for nil db")
	}
}

// --- SessionStore tests ---

func TestSessionStore_GetOrCreate(t *testing.T) {
	db := openDBForTest(t)
	defer db.Close()

	store := NewSessionStore(db)
	ctx := context.Background()
	now := time.Now()

	openID := uniqueID("sess")

	// First call creates
	sess1, err := store.GetOrCreate(ctx, "widget", openID, now)
	if err != nil {
		t.Fatalf("GetOrCreate (create) failed: %v", err)
	}
	if sess1.Channel != "widget" {
		t.Errorf("expected channel widget, got %s", sess1.Channel)
	}
	if sess1.OpenID != openID {
		t.Errorf("expected openID %s, got %s", openID, sess1.OpenID)
	}

	// Second call returns existing
	sess2, err := store.GetOrCreate(ctx, "widget", openID, now)
	if err != nil {
		t.Fatalf("GetOrCreate (get) failed: %v", err)
	}
	if sess2.ID != sess1.ID {
		t.Errorf("expected same ID on second call, got %s vs %s", sess2.ID, sess1.ID)
	}
}

func TestSessionStore_GetOrCreate_NilDB(t *testing.T) {
	store := NewSessionStore(nil)
	_, err := store.GetOrCreate(context.Background(), "widget", "any", time.Now())
	if err == nil {
		t.Error("expected error for nil db")
	}
}

func TestSessionStore_GetByID(t *testing.T) {
	db := openDBForTest(t)
	defer db.Close()

	store := NewSessionStore(db)
	ctx := context.Background()
	now := time.Now()
	openID := uniqueID("sess")

	created, err := store.GetOrCreate(ctx, "widget", openID, now)
	if err != nil {
		t.Fatalf("GetOrCreate failed: %v", err)
	}

	fetched, err := store.GetByID(ctx, created.ID)
	if err != nil {
		t.Fatalf("GetByID failed: %v", err)
	}
	if fetched.ID != created.ID {
		t.Errorf("expected ID %s, got %s", created.ID, fetched.ID)
	}
}

func TestSessionStore_GetByID_NilDB(t *testing.T) {
	store := NewSessionStore(nil)
	_, err := store.GetByID(context.Background(), "any-id")
	if err == nil {
		t.Error("expected error for nil db")
	}
}

func TestSessionStore_Save(t *testing.T) {
	db := openDBForTest(t)
	defer db.Close()

	store := NewSessionStore(db)
	ctx := context.Background()
	now := time.Now()
	openID := uniqueID("sess")

	sess, err := store.GetOrCreate(ctx, "widget", openID, now)
	if err != nil {
		t.Fatalf("GetOrCreate failed: %v", err)
	}

	sess.Status = session.StatusProcessing
	sess.TurnCount = 5
	if err := store.Save(ctx, sess); err != nil {
		t.Fatalf("Save failed: %v", err)
	}

	fetched, err := store.GetByID(ctx, sess.ID)
	if err != nil {
		t.Fatalf("GetByID after Save failed: %v", err)
	}
	if fetched.Status != session.StatusProcessing {
		t.Errorf("expected status processing, got %s", fetched.Status)
	}
	if fetched.TurnCount != 5 {
		t.Errorf("expected turncount 5, got %d", fetched.TurnCount)
	}
}

func TestSessionStore_Save_NilDB(t *testing.T) {
	store := NewSessionStore(nil)
	err := store.Save(context.Background(), &session.Session{})
	if err == nil {
		t.Error("expected error for nil db")
	}
}

// --- AuditStore tests ---

func TestAuditStore_Add(t *testing.T) {
	db := openDBForTest(t)
	defer db.Close()

	store := NewAuditStore(db)
	ctx := context.Background()
	now := time.Now().Truncate(time.Second)

	event := audit.Event{
		ID:          uniqueID("audit"),
		SessionID:   uniqueID("sess"),
		TicketID:    "",
		Type:        "session",
		Action:      "message",
		Channel:     "widget",
		OpenID:      "ou_test",
		ActorID:     "agent-001",
		SourceIP:    "10.0.0.1",
		Payload:     map[string]any{"content": "hello world"},
		BeforeState: map[string]any{"status": "idle"},
		AfterState:  map[string]any{"status": "processing"},
		CreatedAt:   now,
	}

	if err := store.Add(ctx, event); err != nil {
		t.Fatalf("Add failed: %v", err)
	}
}

func TestAuditStore_Add_NilDB(t *testing.T) {
	store := NewAuditStore(nil)
	err := store.Add(context.Background(), audit.Event{Type: "test"})
	if err == nil {
		t.Error("expected error for nil db")
	}
}

func TestAuditStore_Add_TicketScoped(t *testing.T) {
	db := openDBForTest(t)
	defer db.Close()

	store := NewAuditStore(db)
	ctx := context.Background()
	now := time.Now().Truncate(time.Second)

	event := audit.Event{
		ID:          uniqueID("audit"),
		TicketID:    uniqueID("tick"),
		Type:        "ticket",
		Action:      "resolve",
		OpenID:      "ou_test2",
		ActorID:     "agent-002",
		BeforeState: map[string]any{"status": "open"},
		AfterState:  map[string]any{"status": "resolved"},
		CreatedAt:   now,
	}

	if err := store.Add(ctx, event); err != nil {
		t.Fatalf("Add ticket-scoped event failed: %v", err)
	}
}

func TestAuditStore_Add_SystemActor(t *testing.T) {
	db := openDBForTest(t)
	defer db.Close()

	store := NewAuditStore(db)
	ctx := context.Background()

	// Event with no ActorID and no OpenID -> defaults to "system"
	event := audit.Event{
		ID:        uniqueID("audit"),
		SessionID: uniqueID("sess"),
		Type:      "session",
		Action:    "create",
		CreatedAt: time.Now().Truncate(time.Second),
	}

	if err := store.Add(ctx, event); err != nil {
		t.Fatalf("Add system actor event failed: %v", err)
	}
}

func TestAuditStore_Add_EmptyAction(t *testing.T) {
	db := openDBForTest(t)
	defer db.Close()

	store := NewAuditStore(db)
	ctx := context.Background()

	// Empty action should default to "update"
	event := audit.Event{
		ID:        uniqueID("audit"),
		SessionID: uniqueID("sess"),
		Type:      "session",
		CreatedAt: time.Now().Truncate(time.Second),
	}

	if err := store.Add(ctx, event); err != nil {
		t.Fatalf("Add with empty action failed: %v", err)
	}
}
