package lib

import (
	"bytes"
	"os"
	"testing"
	"time"

	"github.com/google/uuid"
)

// testDB creates an in-memory SQLite database with migrations applied.
func testDB(t *testing.T) (*DB, *Config) {
	t.Helper()

	// Create temp file for SQLite (in-memory doesn't work well with WAL)
	tmpFile, err := os.CreateTemp("", "dealspace-test-*.db")
	if err != nil {
		t.Fatalf("create temp file: %v", err)
	}
	tmpFile.Close()
	t.Cleanup(func() { os.Remove(tmpFile.Name()) })

	db, err := OpenDB(tmpFile.Name(), "../migrations/001_initial.sql")
	if err != nil {
		t.Fatalf("OpenDB: %v", err)
	}
	t.Cleanup(func() { db.Close() })

	masterKey := make([]byte, 32)
	for i := range masterKey {
		masterKey[i] = byte(i)
	}

	cfg := &Config{
		MasterKey: masterKey,
		JWTSecret: []byte("test-jwt-secret"),
	}

	return db, cfg
}

// testUser creates a test user with the given role and returns the user ID.
func testUser(t *testing.T, db *DB, cfg *Config, projectID, role string) string {
	t.Helper()

	userID := uuid.New().String()
	now := time.Now().UnixMilli()

	user := &User{
		UserID:    userID,
		Email:     userID + "@test.com",
		Name:      "Test User",
		Password:  "$2a$10$test", // bcrypt placeholder
		OrgID:     "test-org",
		OrgName:   "Test Org",
		Active:    true,
		CreatedAt: now,
		UpdatedAt: now,
	}

	if err := UserCreate(db, user); err != nil {
		t.Fatalf("UserCreate: %v", err)
	}

	// Grant access with appropriate ops based on role
	ops := "r"
	switch role {
	case RoleIBAdmin, RoleIBMember:
		ops = "rwdm"
	case RoleSellerAdmin, RoleSellerMember:
		ops = "rwd"
	case RoleBuyerAdmin, RoleBuyerMember:
		ops = "r"
	}

	access := &Access{
		ID:        uuid.New().String(),
		ProjectID: projectID,
		UserID:    userID,
		Role:      role,
		Ops:       ops,
		CanGrant:  role == RoleIBAdmin || role == RoleSellerAdmin,
		GrantedBy: "system",
		GrantedAt: now,
	}

	if err := AccessGrant(db, access); err != nil {
		t.Fatalf("AccessGrant: %v", err)
	}

	return userID
}

// testProject creates a test project entry and returns the project ID.
func testProject(t *testing.T, db *DB, cfg *Config, ownerID string) string {
	t.Helper()

	projectID := uuid.New().String()
	now := time.Now().UnixMilli()

	// First grant the owner access to create entries
	access := &Access{
		ID:        uuid.New().String(),
		ProjectID: projectID,
		UserID:    ownerID,
		Role:      RoleIBAdmin,
		Ops:       "rwdm",
		CanGrant:  true,
		GrantedBy: "system",
		GrantedAt: now,
	}
	if err := AccessGrant(db, access); err != nil {
		t.Fatalf("AccessGrant for owner: %v", err)
	}

	// Create project entry
	entry := &Entry{
		ProjectID:   projectID,
		Type:        TypeProject,
		Depth:       0,
		Stage:       StagePreDataroom,
		SummaryText: "Test Project",
		DataText:    `{"name": "Test Project"}`,
	}

	if err := EntryWrite(db, cfg, ownerID, entry); err != nil {
		t.Fatalf("EntryWrite project: %v", err)
	}

	return projectID
}

func TestEntryWriteRead(t *testing.T) {
	db, cfg := testDB(t)

	// Create owner user first
	ownerID := uuid.New().String()
	now := time.Now().UnixMilli()
	owner := &User{
		UserID:    ownerID,
		Email:     "owner@test.com",
		Name:      "Owner",
		Password:  "$2a$10$test",
		OrgID:     "test-org",
		OrgName:   "Test Org",
		Active:    true,
		CreatedAt: now,
		UpdatedAt: now,
	}
	if err := UserCreate(db, owner); err != nil {
		t.Fatalf("UserCreate: %v", err)
	}

	projectID := testProject(t, db, cfg, ownerID)

	// Write an entry
	entry := &Entry{
		ProjectID:   projectID,
		Type:        TypeRequest,
		Depth:       1,
		Stage:       StagePreDataroom,
		SummaryText: "Test Summary",
		DataText:    `{"question": "What is the answer?"}`,
	}

	if err := EntryWrite(db, cfg, ownerID, entry); err != nil {
		t.Fatalf("EntryWrite: %v", err)
	}

	if entry.EntryID == "" {
		t.Error("EntryID should be set after write")
	}
	if entry.Version != 1 {
		t.Errorf("Version should be 1, got %d", entry.Version)
	}

	// Read it back
	filter := EntryFilter{
		ProjectID: projectID,
		Type:      TypeRequest,
	}
	entries, err := EntryRead(db, cfg, ownerID, projectID, filter)
	if err != nil {
		t.Fatalf("EntryRead: %v", err)
	}

	if len(entries) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(entries))
	}

	got := entries[0]
	if got.EntryID != entry.EntryID {
		t.Errorf("EntryID mismatch: got %s, want %s", got.EntryID, entry.EntryID)
	}
	if got.SummaryText != "Test Summary" {
		t.Errorf("SummaryText mismatch: got %q, want %q", got.SummaryText, "Test Summary")
	}
	if got.DataText != `{"question": "What is the answer?"}` {
		t.Errorf("DataText mismatch: got %q", got.DataText)
	}
}

func TestEntryReadAccessDenied(t *testing.T) {
	db, cfg := testDB(t)

	// Create owner and project
	ownerID := uuid.New().String()
	now := time.Now().UnixMilli()
	owner := &User{
		UserID:    ownerID,
		Email:     "owner@test.com",
		Name:      "Owner",
		Password:  "$2a$10$test",
		Active:    true,
		CreatedAt: now,
		UpdatedAt: now,
	}
	if err := UserCreate(db, owner); err != nil {
		t.Fatalf("UserCreate: %v", err)
	}

	projectID := testProject(t, db, cfg, ownerID)

	// Create another user with NO access to this project
	noAccessUser := &User{
		UserID:    uuid.New().String(),
		Email:     "noaccess@test.com",
		Name:      "No Access",
		Password:  "$2a$10$test",
		Active:    true,
		CreatedAt: now,
		UpdatedAt: now,
	}
	if err := UserCreate(db, noAccessUser); err != nil {
		t.Fatalf("UserCreate: %v", err)
	}

	// Try to read with no-access user
	filter := EntryFilter{ProjectID: projectID}
	_, err := EntryRead(db, cfg, noAccessUser.UserID, projectID, filter)
	if err != ErrAccessDenied {
		t.Errorf("expected ErrAccessDenied, got %v", err)
	}
}

func TestSoftDelete(t *testing.T) {
	db, cfg := testDB(t)

	// Setup
	ownerID := uuid.New().String()
	now := time.Now().UnixMilli()
	owner := &User{
		UserID:    ownerID,
		Email:     "owner@test.com",
		Name:      "Owner",
		Password:  "$2a$10$test",
		Active:    true,
		CreatedAt: now,
		UpdatedAt: now,
	}
	UserCreate(db, owner)
	projectID := testProject(t, db, cfg, ownerID)

	// Create entry
	entry := &Entry{
		ProjectID:   projectID,
		Type:        TypeRequest,
		Depth:       1,
		Stage:       StagePreDataroom,
		SummaryText: "To be deleted",
	}
	EntryWrite(db, cfg, ownerID, entry)
	entryID := entry.EntryID

	// Delete it
	if err := EntryDelete(db, ownerID, projectID, entryID); err != nil {
		t.Fatalf("EntryDelete: %v", err)
	}

	// Verify not returned in normal reads
	filter := EntryFilter{ProjectID: projectID, Type: TypeRequest}
	entries, err := EntryRead(db, cfg, ownerID, projectID, filter)
	if err != nil {
		t.Fatalf("EntryRead: %v", err)
	}
	for _, e := range entries {
		if e.EntryID == entryID {
			t.Error("deleted entry should not be returned in normal reads")
		}
	}

	// Verify deleted_at is set via direct query
	var deletedAt *int64
	err = db.Conn.QueryRow("SELECT deleted_at FROM entries WHERE entry_id = ?", entryID).Scan(&deletedAt)
	if err != nil {
		t.Fatalf("query deleted_at: %v", err)
	}
	if deletedAt == nil {
		t.Error("deleted_at should be set")
	}
}

func TestOptimisticLocking(t *testing.T) {
	db, cfg := testDB(t)

	// Setup
	ownerID := uuid.New().String()
	now := time.Now().UnixMilli()
	owner := &User{
		UserID:    ownerID,
		Email:     "owner@test.com",
		Name:      "Owner",
		Password:  "$2a$10$test",
		Active:    true,
		CreatedAt: now,
		UpdatedAt: now,
	}
	UserCreate(db, owner)
	projectID := testProject(t, db, cfg, ownerID)

	// Create entry v1
	entry := &Entry{
		ProjectID:   projectID,
		Type:        TypeRequest,
		Depth:       1,
		Stage:       StagePreDataroom,
		SummaryText: "Version 1",
	}
	EntryWrite(db, cfg, ownerID, entry)
	if entry.Version != 1 {
		t.Fatalf("initial version should be 1, got %d", entry.Version)
	}

	// Update successfully to v2
	entry.SummaryText = "Version 2"
	if err := EntryWrite(db, cfg, ownerID, entry); err != nil {
		t.Fatalf("EntryWrite v2: %v", err)
	}
	if entry.Version != 2 {
		t.Fatalf("version should be 2, got %d", entry.Version)
	}

	// Try to update with stale version (v1)
	staleEntry := &Entry{
		EntryID:     entry.EntryID,
		ProjectID:   projectID,
		Type:        TypeRequest,
		Depth:       1,
		Stage:       StagePreDataroom,
		SummaryText: "Stale update",
		Version:     1, // Stale!
	}
	err := EntryWrite(db, cfg, ownerID, staleEntry)
	if err != ErrVersionConflict {
		t.Errorf("expected ErrVersionConflict, got %v", err)
	}
}

func TestBlindIndexSearch(t *testing.T) {
	db, cfg := testDB(t)

	// Setup
	ownerID := uuid.New().String()
	now := time.Now().UnixMilli()
	owner := &User{
		UserID:    ownerID,
		Email:     "owner@test.com",
		Name:      "Owner",
		Password:  "$2a$10$test",
		Active:    true,
		CreatedAt: now,
		UpdatedAt: now,
	}
	UserCreate(db, owner)
	projectID := testProject(t, db, cfg, ownerID)

	// Create entry with search key
	hmacKey, _ := DeriveHMACKey(cfg.MasterKey, projectID)
	searchTerm := "unique-search-term-123"
	blindIdx := BlindIndex(hmacKey, searchTerm)

	entry := &Entry{
		ProjectID:   projectID,
		Type:        TypeRequest,
		Depth:       1,
		Stage:       StagePreDataroom,
		SummaryText: "Searchable Entry",
		SearchKey:   blindIdx,
	}
	EntryWrite(db, cfg, ownerID, entry)

	// Search by blind index
	filter := EntryFilter{
		ProjectID: projectID,
		SearchKey: blindIdx,
	}
	entries, err := EntryRead(db, cfg, ownerID, projectID, filter)
	if err != nil {
		t.Fatalf("EntryRead with SearchKey: %v", err)
	}

	if len(entries) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(entries))
	}
	if entries[0].EntryID != entry.EntryID {
		t.Error("wrong entry returned")
	}

	// Search with different blind index should return nothing
	wrongIdx := BlindIndex(hmacKey, "wrong-term")
	filter.SearchKey = wrongIdx
	entries, err = EntryRead(db, cfg, ownerID, projectID, filter)
	if err != nil {
		t.Fatalf("EntryRead with wrong SearchKey: %v", err)
	}
	if len(entries) != 0 {
		t.Errorf("expected 0 entries with wrong search key, got %d", len(entries))
	}
}

func TestUserByEmail(t *testing.T) {
	db, _ := testDB(t)

	now := time.Now().UnixMilli()
	user := &User{
		UserID:    uuid.New().String(),
		Email:     "test@example.com",
		Name:      "Test User",
		Password:  "$2a$10$test",
		OrgID:     "test-org",
		OrgName:   "Test Org",
		Active:    true,
		CreatedAt: now,
		UpdatedAt: now,
	}

	if err := UserCreate(db, user); err != nil {
		t.Fatalf("UserCreate: %v", err)
	}

	// Find by email
	found, err := UserByEmail(db, "test@example.com")
	if err != nil {
		t.Fatalf("UserByEmail: %v", err)
	}
	if found == nil {
		t.Fatal("user not found")
	}
	if found.UserID != user.UserID {
		t.Error("wrong user returned")
	}

	// Not found
	notFound, err := UserByEmail(db, "nonexistent@example.com")
	if err != nil {
		t.Fatalf("UserByEmail nonexistent: %v", err)
	}
	if notFound != nil {
		t.Error("should return nil for nonexistent email")
	}
}

func TestSessionCreateAndRevoke(t *testing.T) {
	db, _ := testDB(t)

	now := time.Now().UnixMilli()
	user := &User{
		UserID:    uuid.New().String(),
		Email:     "session@test.com",
		Name:      "Session User",
		Password:  "$2a$10$test",
		Active:    true,
		CreatedAt: now,
		UpdatedAt: now,
	}
	UserCreate(db, user)

	session := &Session{
		ID:          uuid.New().String(),
		UserID:      user.UserID,
		Fingerprint: "test-fingerprint",
		CreatedAt:   now,
		ExpiresAt:   now + 86400000, // +1 day
		Revoked:     false,
	}

	if err := SessionCreate(db, session); err != nil {
		t.Fatalf("SessionCreate: %v", err)
	}

	// Retrieve session
	found, err := SessionByID(db, session.ID)
	if err != nil {
		t.Fatalf("SessionByID: %v", err)
	}
	if found == nil {
		t.Fatal("session not found")
	}
	if found.Revoked {
		t.Error("session should not be revoked")
	}

	// Revoke
	if err := SessionRevoke(db, session.ID); err != nil {
		t.Fatalf("SessionRevoke: %v", err)
	}

	// Check revoked
	found, _ = SessionByID(db, session.ID)
	if !found.Revoked {
		t.Error("session should be revoked")
	}
}

func TestBuyerCannotSeePreDataroom(t *testing.T) {
	db, cfg := testDB(t)

	// Setup owner
	ownerID := uuid.New().String()
	now := time.Now().UnixMilli()
	owner := &User{
		UserID:    ownerID,
		Email:     "owner@test.com",
		Name:      "Owner",
		Password:  "$2a$10$test",
		Active:    true,
		CreatedAt: now,
		UpdatedAt: now,
	}
	UserCreate(db, owner)
	projectID := testProject(t, db, cfg, ownerID)

	// Create pre_dataroom entry
	preEntry := &Entry{
		ProjectID:   projectID,
		Type:        TypeRequest,
		Depth:       1,
		Stage:       StagePreDataroom,
		SummaryText: "Pre-dataroom entry",
	}
	EntryWrite(db, cfg, ownerID, preEntry)

	// Create dataroom entry
	drEntry := &Entry{
		ProjectID:   projectID,
		Type:        TypeRequest,
		Depth:       1,
		Stage:       StageDataroom,
		SummaryText: "Dataroom entry",
	}
	EntryWrite(db, cfg, ownerID, drEntry)

	// Create buyer user
	buyerID := testUser(t, db, cfg, projectID, RoleBuyerMember)

	// Buyer reads entries
	filter := EntryFilter{ProjectID: projectID, Type: TypeRequest}
	entries, err := EntryRead(db, cfg, buyerID, projectID, filter)
	if err != nil {
		t.Fatalf("EntryRead as buyer: %v", err)
	}

	// Should only see dataroom entry
	if len(entries) != 1 {
		t.Errorf("buyer should see 1 entry (dataroom only), got %d", len(entries))
	}
	for _, e := range entries {
		if e.Stage == StagePreDataroom {
			t.Error("buyer should not see pre_dataroom entries")
		}
	}

	// Owner should see both
	entries, _ = EntryRead(db, cfg, ownerID, projectID, filter)
	if len(entries) != 2 {
		t.Errorf("owner should see 2 entries, got %d", len(entries))
	}
}

func TestLocalStore(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "dealspace-store-test")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := NewLocalStore(tmpDir)
	if err != nil {
		t.Fatalf("NewLocalStore: %v", err)
	}

	// Write
	data := []byte("test object data")
	projectID := "test-project"
	id, err := store.Write(projectID, data)
	if err != nil {
		t.Fatalf("Write: %v", err)
	}
	if id == "" {
		t.Error("Write should return an ID")
	}

	// Exists
	if !store.Exists(id) {
		t.Error("object should exist")
	}

	// Read
	read, err := store.Read(projectID, id)
	if err != nil {
		t.Fatalf("Read: %v", err)
	}
	if !bytes.Equal(read, data) {
		t.Error("data mismatch")
	}

	// Delete
	if err := store.Delete(id); err != nil {
		t.Fatalf("Delete: %v", err)
	}
	if store.Exists(id) {
		t.Error("object should not exist after delete")
	}

	// Read nonexistent
	_, err = store.Read(projectID, "nonexistent")
	if err != ErrObjectNotFound {
		t.Errorf("expected ErrObjectNotFound, got %v", err)
	}
}
