package mysql

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

	afterflightinspection "wucher/internal/domain/after_flight_inspection"
	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/domain/helicopter"
	"wucher/internal/shared/pkg/uuidv7"
)

func intPtr(v int) *int {
	return &v
}

func openHelicopterTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	dsn := fmt.Sprintf("file:helicopter_test_%d?mode=memory&cache=shared", time.Now().UnixNano())
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
		NowFunc: func() time.Time { return time.Now().UTC() },
	})
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}
	if err := db.AutoMigrate(&filemanager.Folder{}, &filemanager.File{}, &filemanager.Attachment{}, &helicopter.Helicopter{}); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
	return db
}

func TestNewHelicopterRepository(t *testing.T) {
	db := openHelicopterTestDB(t)
	repo := NewHelicopterRepository(db)
	if repo == nil || repo.db == nil {
		t.Fatalf("expected repository initialized")
	}
	if repo.hasUsersTable {
		t.Fatalf("expected users table cache to be false in base helicopter test db")
	}
}

func TestHelicopterRepositoryCachesUsersTablePresence(t *testing.T) {
	db := openHelicopterTestDB(t)
	if err := db.Exec(`CREATE TABLE users (
		id BLOB PRIMARY KEY,
		first_name TEXT,
		last_name TEXT,
		username TEXT,
		email TEXT
	)`).Error; err != nil {
		t.Fatalf("create users table: %v", err)
	}

	repo := NewHelicopterRepository(db)
	if !repo.hasUsersTable {
		t.Fatalf("expected users table cache to be true")
	}

	creatorID := uuidv7.MustBytes()
	updaterID := uuidv7.MustBytes()
	if err := db.Table("users").Create([]map[string]any{
		{
			"id":         creatorID,
			"first_name": "Ada",
			"last_name":  "Lovelace",
			"username":   "ada",
			"email":      "ada@example.com",
		},
		{
			"id":         updaterID,
			"first_name": "Grace",
			"last_name":  "Hopper",
			"username":   "grace",
			"email":      "grace@example.com",
		},
	}).Error; err != nil {
		t.Fatalf("seed users: %v", err)
	}

	row := &helicopter.Helicopter{
		Designation: "H145",
		Identifier:  "PK-AUDIT",
		Type:        "Twin",
		CreatedBy:   creatorID,
		UpdatedBy:   updaterID,
	}
	if err := repo.Create(context.Background(), row); err != nil {
		t.Fatalf("seed helicopter: %v", err)
	}

	got, err := repo.GetByID(context.Background(), row.ID)
	if err != nil {
		t.Fatalf("get helicopter: %v", err)
	}
	if got == nil {
		t.Fatalf("expected helicopter row")
	}
	if got.CreatedByName != "Ada Lovelace" {
		t.Fatalf("unexpected created_by_name: %q", got.CreatedByName)
	}
	if got.UpdatedByName != "Grace Hopper" {
		t.Fatalf("unexpected updated_by_name: %q", got.UpdatedByName)
	}
}

func TestHelicopterRepositoryCreate(t *testing.T) {
	db := openHelicopterTestDB(t)
	repo := NewHelicopterRepository(db)
	row := &helicopter.Helicopter{Designation: "H145", Identifier: "PK-ABC", Type: "Twin"}

	if err := repo.Create(context.Background(), row); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(row.ID) == 0 {
		t.Fatalf("expected id set")
	}
	inactive := &helicopter.Helicopter{Designation: "Inactive", Identifier: "PK-INACTIVE", Type: "Single", IsActive: false}
	if err := repo.Create(context.Background(), inactive); err != nil {
		t.Fatalf("create inactive: %v", err)
	}
	gotInactive, err := repo.GetByID(context.Background(), inactive.ID)
	if err != nil {
		t.Fatalf("get inactive: %v", err)
	}
	if gotInactive == nil || gotInactive.IsActive {
		t.Fatalf("expected inactive helicopter persisted as false")
	}

	sqlDB, _ := db.DB()
	_ = sqlDB.Close()
	if err := repo.Create(context.Background(), &helicopter.Helicopter{Designation: "AfterClose", Identifier: "PK-CLOSE", Type: "Twin"}); err == nil {
		t.Fatalf("expected error on closed DB")
	}
}

func TestHelicopterRepositoryUpdate(t *testing.T) {
	db := openHelicopterTestDB(t)
	repo := NewHelicopterRepository(db)
	row := &helicopter.Helicopter{Designation: "Old", Identifier: "PK-OLD", Type: "Single"}
	if err := repo.Create(context.Background(), row); err != nil {
		t.Fatalf("seed create: %v", err)
	}

	row.Designation = "New"
	row.Type = "Twin"
	if err := repo.Update(context.Background(), row); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	loaded, err := repo.GetByID(context.Background(), row.ID)
	if err != nil || loaded == nil || loaded.Designation != "New" || loaded.Type != "Twin" {
		t.Fatalf("expected updated row")
	}

	sqlDB, _ := db.DB()
	_ = sqlDB.Close()
	if err := repo.Update(context.Background(), row); err == nil {
		t.Fatalf("expected error on closed DB")
	}
}

func TestHelicopterRepositoryUpdate_ReplacesFotoAttachmentID(t *testing.T) {
	db := openHelicopterTestDB(t)
	repo := NewHelicopterRepository(db)
	now := time.Now().UTC()

	folder := &filemanager.Folder{
		ID:             uuidv7.MustBytes(),
		Name:           "photos",
		NameNormalized: "photos",
		Depth:          0,
		PathCache:      "/photos",
		NameSlot:       "live",
		CreatedAt:      now,
		UpdatedAt:      now,
	}
	if err := db.Create(folder).Error; err != nil {
		t.Fatalf("create folder: %v", err)
	}

	makeFile := func(name, key string) *filemanager.File {
		return &filemanager.File{
			ID:             uuidv7.MustBytes(),
			FolderID:       folder.ID,
			Name:           name,
			NameNormalized: strings.ToLower(name),
			Extension:      "webp",
			SizeBytes:      1234,
			MimeType:       "image/webp",
			Bucket:         "bucket",
			ObjectKey:      key,
			Status:         filemanager.FileStatusReady,
			NameSlot:       "live",
			CreatedAt:      now,
			UpdatedAt:      now,
		}
	}
	oldFile := makeFile("old.webp", "objects/old.webp")
	newFile := makeFile("new.webp", "objects/new.webp")
	if err := db.Create(oldFile).Error; err != nil {
		t.Fatalf("create old file: %v", err)
	}
	if err := db.Create(newFile).Error; err != nil {
		t.Fatalf("create new file: %v", err)
	}

	oldAttachment := &filemanager.Attachment{
		ID:        uuidv7.MustBytes(),
		FileID:    oldFile.ID,
		RefType:   "helicopter_photo",
		RefID:     "helicopter-1",
		IsPrimary: true,
		CreatedAt: now,
		UpdatedAt: now,
	}
	newAttachment := &filemanager.Attachment{
		ID:        uuidv7.MustBytes(),
		FileID:    newFile.ID,
		RefType:   "helicopter_photo",
		RefID:     "helicopter-1",
		IsPrimary: true,
		CreatedAt: now,
		UpdatedAt: now,
	}
	if err := db.Create(oldAttachment).Error; err != nil {
		t.Fatalf("create old attachment: %v", err)
	}
	if err := db.Create(newAttachment).Error; err != nil {
		t.Fatalf("create new attachment: %v", err)
	}

	row := &helicopter.Helicopter{
		ID:               uuidv7.MustBytes(),
		Designation:      "Old",
		Identifier:       "PK-OLD-ATT",
		Type:             "Single",
		FotoAttachmentID: oldAttachment.ID,
		FotoAttachment:   oldAttachment,
		CreatedAt:        now,
		UpdatedAt:        now,
	}
	if err := repo.Create(context.Background(), row); err != nil {
		t.Fatalf("seed create helicopter: %v", err)
	}

	row.FotoAttachmentID = newAttachment.ID
	row.FotoAttachment = oldAttachment
	if err := repo.Update(context.Background(), row); err != nil {
		t.Fatalf("update helicopter: %v", err)
	}

	loaded, err := repo.GetByID(context.Background(), row.ID)
	if err != nil {
		t.Fatalf("get by id after update: %v", err)
	}
	if loaded == nil {
		t.Fatalf("expected updated row")
	}
	if string(loaded.FotoAttachmentID) != string(newAttachment.ID) {
		t.Fatalf("expected foto_attachment_id replaced")
	}
}

func TestHelicopterRepositoryDelete(t *testing.T) {
	db := openHelicopterTestDB(t)
	repo := NewHelicopterRepository(db)
	row := &helicopter.Helicopter{Designation: "DeleteMe", Identifier: "PK-DEL", Type: "Twin"}
	if err := repo.Create(context.Background(), row); err != nil {
		t.Fatalf("seed create: %v", err)
	}

	if err := repo.Delete(context.Background(), row.ID); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	got, err := repo.GetByID(context.Background(), row.ID)
	if err != nil {
		t.Fatalf("get by id after delete: %v", err)
	}
	if got != nil {
		t.Fatalf("expected row deleted")
	}
}

func TestHelicopterRepositoryGetByID(t *testing.T) {
	t.Run("found", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		row := &helicopter.Helicopter{Designation: "Found", Identifier: "PK-FND", Type: "Twin"}
		if err := repo.Create(context.Background(), row); err != nil {
			t.Fatalf("seed create: %v", err)
		}

		got, err := repo.GetByID(context.Background(), row.ID)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if got == nil || got.Identifier != "PK-FND" {
			t.Fatalf("expected row found")
		}
	})

	t.Run("not found", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		got, err := repo.GetByID(context.Background(), uuidv7.MustBytes())
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if got != nil {
			t.Fatalf("expected nil for not found")
		}
	})

	t.Run("db error", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		sqlDB, _ := db.DB()
		_ = sqlDB.Close()

		if _, err := repo.GetByID(context.Background(), uuidv7.MustBytes()); err == nil {
			t.Fatalf("expected error on closed DB")
		}
	})
}

func TestHelicopterRepositoryList(t *testing.T) {
	t.Run("success without limit", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "H145", Identifier: "PK-A", Type: "Twin"})
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "H125", Identifier: "PK-B", Type: "Single"})

		rows, total, err := repo.List(context.Background(), "", nil, "", 0, 0, nil)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if total != 2 || len(rows) != 2 {
			t.Fatalf("expected 2 rows, total=%d len=%d", total, len(rows))
		}
	})

	t.Run("success with filter sort and limit", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Main", Identifier: "PK-MAIN", Type: "Twin"})
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Backup", Identifier: "PK-BACK", Type: "Single"})

		rows, total, err := repo.List(context.Background(), "MAIN", nil, "identifier DESC", 1, 0, nil)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if total != 1 || len(rows) != 1 || rows[0].Identifier != "PK-MAIN" {
			t.Fatalf("unexpected list result")
		}
	})

	t.Run("default order active sortkey first and inactive last", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Gamma", Identifier: "PK-G", Type: "Twin", IsActive: true})
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Beta", Identifier: "PK-B0", Type: "Twin", SortKey: intPtr(0), IsActive: true})
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Charlie", Identifier: "PK-C", Type: "Twin", SortKey: intPtr(2), IsActive: true})
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Alpha", Identifier: "PK-A1", Type: "Twin", IsActive: true})
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Alpha", Identifier: "PK-A2", Type: "Twin", SortKey: intPtr(1), IsActive: true})
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Zulu", Identifier: "PK-Z", Type: "Twin", IsActive: false})
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Bravo", Identifier: "PK-B", Type: "Twin", IsActive: false, SortKey: intPtr(9)})

		rows, total, err := repo.List(context.Background(), "", nil, "", 0, 0, nil)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if total != 7 || len(rows) != 7 {
			t.Fatalf("unexpected total/len total=%d len=%d", total, len(rows))
		}

		gotOrder := []string{
			rows[0].Designation + "|" + rows[0].Identifier,
			rows[1].Designation + "|" + rows[1].Identifier,
			rows[2].Designation + "|" + rows[2].Identifier,
			rows[3].Designation + "|" + rows[3].Identifier,
			rows[4].Designation + "|" + rows[4].Identifier,
			rows[5].Designation + "|" + rows[5].Identifier,
			rows[6].Designation + "|" + rows[6].Identifier,
		}
		wantOrder := []string{
			"Beta|PK-B0",
			"Alpha|PK-A2",
			"Charlie|PK-C",
			"Alpha|PK-A1",
			"Gamma|PK-G",
			"Bravo|PK-B",
			"Zulu|PK-Z",
		}
		for i := range wantOrder {
			if gotOrder[i] != wantOrder[i] {
				t.Fatalf("unexpected default order: got=%v want=%v", gotOrder, wantOrder)
			}
		}
	})

	t.Run("status filter", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)

		// Minimal schema for the booked/available EXISTS subquery.
		if err := db.Exec(`CREATE TABLE reserve_acs (id BLOB PRIMARY KEY, helicopter_id BLOB, inspection_id BLOB, deleted_at DATETIME)`).Error; err != nil {
			t.Fatalf("create reserve_acs: %v", err)
		}
		if err := db.Exec(`CREATE TABLE takeover_acs (id BLOB PRIMARY KEY, reserve_ac_id BLOB, deleted_at DATETIME)`).Error; err != nil {
			t.Fatalf("create takeover_acs: %v", err)
		}
		if err := db.Exec(`CREATE TABLE flights (id BLOB PRIMARY KEY, reserve_ac_id BLOB, takeover_ac_id BLOB, deleted_at DATETIME)`).Error; err != nil {
			t.Fatalf("create flights: %v", err)
		}
		if err := db.AutoMigrate(&afterflightinspection.AfterFlightInspection{}); err != nil {
			t.Fatalf("auto migrate after flight inspection: %v", err)
		}

		aog := &helicopter.Helicopter{Designation: "AOG", Identifier: "PK-AOG", Type: "Twin", AirOnGround: true}
		booked := &helicopter.Helicopter{Designation: "Booked", Identifier: "PK-BKD", Type: "Twin"}
		closed := &helicopter.Helicopter{Designation: "Closed", Identifier: "PK-CLS", Type: "Twin"}
		available := &helicopter.Helicopter{Designation: "Avail", Identifier: "PK-AVL", Type: "Twin"}
		for _, h := range []*helicopter.Helicopter{aog, booked, closed, available} {
			if err := repo.Create(context.Background(), h); err != nil {
				t.Fatalf("create helicopter: %v", err)
			}
		}

		// Give "booked" an active (non-deleted) flight assignment.
		raID := uuidv7.MustBytes()
		takeoverID := uuidv7.MustBytes()
		if err := db.Table("reserve_acs").Create(map[string]any{"id": raID, "helicopter_id": booked.ID}).Error; err != nil {
			t.Fatalf("insert reserve_ac: %v", err)
		}
		if err := db.Table("takeover_acs").Create(map[string]any{"id": takeoverID, "reserve_ac_id": raID}).Error; err != nil {
			t.Fatalf("insert takeover_ac: %v", err)
		}
		if err := db.Table("flights").Create(map[string]any{"id": uuidv7.MustBytes(), "takeover_ac_id": takeoverID}).Error; err != nil {
			t.Fatalf("insert flight: %v", err)
		}
		closedRAID := uuidv7.MustBytes()
		closedTakeoverID := uuidv7.MustBytes()
		closedInspectionID := uuidv7.MustBytes()
		if err := db.Table("reserve_acs").Create(map[string]any{"id": closedRAID, "helicopter_id": closed.ID, "inspection_id": closedInspectionID}).Error; err != nil {
			t.Fatalf("insert closed reserve_ac: %v", err)
		}
		if err := db.Table("takeover_acs").Create(map[string]any{"id": closedTakeoverID, "reserve_ac_id": closedRAID}).Error; err != nil {
			t.Fatalf("insert closed takeover_ac: %v", err)
		}
		if err := db.Table("flights").Create(map[string]any{"id": uuidv7.MustBytes(), "takeover_ac_id": closedTakeoverID}).Error; err != nil {
			t.Fatalf("insert closed flight: %v", err)
		}
		if err := db.Create(&afterflightinspection.AfterFlightInspection{FlightInspectionID: closedInspectionID, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}).Error; err != nil {
			t.Fatalf("insert after flight inspection: %v", err)
		}

		cases := []struct {
			name        string
			statuses    []string
			groundedIDs [][]byte
			want        []string
		}{
			{name: "aog only", statuses: []string{"aog"}, want: []string{"PK-AOG"}},
			{name: "booked only", statuses: []string{"booked"}, want: []string{"PK-BKD"}},
			{name: "available only", statuses: []string{"available"}, want: []string{"PK-AVL", "PK-CLS"}},
			{name: "available and booked", statuses: []string{"available", "booked"}, want: []string{"PK-AVL", "PK-BKD", "PK-CLS"}},
			{name: "mcf matches nothing", statuses: []string{"mcf"}, want: []string{}},
			{name: "no filter returns all", statuses: nil, want: []string{"PK-AOG", "PK-AVL", "PK-BKD", "PK-CLS"}},
			{name: "complaint-grounded excluded from available", statuses: []string{"available"}, groundedIDs: [][]byte{available.ID}, want: []string{"PK-CLS"}},
			{name: "complaint-grounded appears as aog", statuses: []string{"aog"}, groundedIDs: [][]byte{available.ID}, want: []string{"PK-AOG", "PK-AVL"}},
		}

		for _, tc := range cases {
			t.Run(tc.name, func(t *testing.T) {
				rows, total, err := repo.List(context.Background(), "", tc.statuses, "identifier ASC", 0, 0, tc.groundedIDs)
				if err != nil {
					t.Fatalf("list: %v", err)
				}
				if int(total) != len(tc.want) || len(rows) != len(tc.want) {
					t.Fatalf("count mismatch: total=%d len=%d want=%d", total, len(rows), len(tc.want))
				}
				got := make([]string, len(rows))
				for i := range rows {
					got[i] = rows[i].Identifier
				}
				for i := range tc.want {
					if got[i] != tc.want[i] {
						t.Fatalf("unexpected rows: got=%v want=%v", got, tc.want)
					}
				}
			})
		}
	})

	t.Run("count error", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		if err := db.Migrator().DropTable(&helicopter.Helicopter{}); err != nil {
			t.Fatalf("drop table: %v", err)
		}

		if _, _, err := repo.List(context.Background(), "", nil, "", 10, 0, nil); err == nil {
			t.Fatalf("expected count error")
		}
	})

	t.Run("find error", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Main", Identifier: "PK-ERR", Type: "Twin"})

		if _, _, err := repo.List(context.Background(), "", nil, "designation ASC, )", 10, 0, nil); err == nil {
			t.Fatalf("expected find error from invalid sort")
		}
	})
}

func TestHelicopterRepositoryNextReportNumber(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		row := &helicopter.Helicopter{
			Designation:    "H145",
			Identifier:     " PK-ABC ",
			Type:           "Twin",
			ReportSequence: 7,
		}
		if err := repo.Create(context.Background(), row); err != nil {
			t.Fatalf("seed create: %v", err)
		}

		reportNumber, sequence, err := repo.NextReportNumber(context.Background(), row.ID)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if reportNumber != "PK-ABC-0008" || sequence != 8 {
			t.Fatalf("unexpected report number/sequence: %q %d", reportNumber, sequence)
		}
		loaded, err := repo.GetByID(context.Background(), row.ID)
		if err != nil || loaded == nil || loaded.ReportSequence != 8 {
			t.Fatalf("expected report_sequence updated")
		}
	})

	t.Run("identifier empty", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		row := &helicopter.Helicopter{
			Designation: "H145",
			Identifier:  "   ",
			Type:        "Twin",
		}
		if err := repo.Create(context.Background(), row); err != nil {
			t.Fatalf("seed create: %v", err)
		}

		if _, _, err := repo.NextReportNumber(context.Background(), row.ID); err == nil {
			t.Fatalf("expected identifier empty error")
		}
	})

	t.Run("not found", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		if _, _, err := repo.NextReportNumber(context.Background(), uuidv7.MustBytes()); err == nil {
			t.Fatalf("expected not found error")
		}
	})

	t.Run("db error", func(t *testing.T) {
		db := openHelicopterTestDB(t)
		repo := NewHelicopterRepository(db)
		sqlDB, _ := db.DB()
		_ = sqlDB.Close()

		if _, _, err := repo.NextReportNumber(context.Background(), uuidv7.MustBytes()); err == nil {
			t.Fatalf("expected error on closed DB")
		}
	})
}
