package handlers

import (
	"bytes"
	"context"
	"errors"
	"io"
	"net/http"
	"strings"
	"testing"
	"time"

	mysqldriver "github.com/go-sql-driver/mysql"
	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"

	facilitydomain "wucher/internal/domain/facility"
	flightdata "wucher/internal/domain/flight_data"
	hospitaldomain "wucher/internal/domain/hospital"
	icaodomain "wucher/internal/domain/icao"
	missiondomain "wucher/internal/domain/mission"
	userdomain "wucher/internal/domain/user"
	"wucher/internal/shared/pkg/uuidv7"
)

type flightDataServiceMock struct {
	createFn      func(context.Context, *flightdata.FlightData) error
	updateFn      func(context.Context, *flightdata.FlightData) error
	deleteFn      func(context.Context, []byte, []byte) error
	getByIDFn     func(context.Context, []byte) (*flightdata.FlightData, error)
	getByFlightFn func(context.Context, []byte) (*flightdata.FlightData, error)
	getByMisFn    func(context.Context, []byte) (*flightdata.FlightData, error)
	listFn        func(context.Context, string, []byte, []byte, int, int) ([]flightdata.FlightData, int64, error)
	getSpoFn      func(context.Context, []byte) (*flightdata.SPODetails, error)
}

type userServiceMock struct {
	getByIDFn func(context.Context, []byte) (*userdomain.User, error)
}

type facilityServiceMock struct {
	getByIDFn func(context.Context, []byte) (*facilitydomain.Facility, error)
}

type flightDataMissionServiceMock struct {
	getByIDFn func(context.Context, []byte) (*missiondomain.Mission, error)
}

type hospitalServiceMock struct {
	getByIDFn func(context.Context, []byte) (*hospitaldomain.Hospital, error)
}

type icaoServiceMock struct {
	getByIDFn func(context.Context, []byte) (*icaodomain.ICAO, error)
}

func (m *flightDataServiceMock) Create(ctx context.Context, row *flightdata.FlightData) error {
	if m.createFn != nil {
		return m.createFn(ctx, row)
	}
	return nil
}
func (m *flightDataServiceMock) CreatePlaceholder(ctx context.Context, row *flightdata.FlightData) error {
	if m.createFn != nil {
		return m.createFn(ctx, row)
	}
	return nil
}
func (m *flightDataServiceMock) Update(ctx context.Context, row *flightdata.FlightData) error {
	if m.updateFn != nil {
		return m.updateFn(ctx, row)
	}
	return nil
}
func (m *flightDataServiceMock) Delete(ctx context.Context, id []byte, deletedBy []byte) error {
	if m.deleteFn != nil {
		return m.deleteFn(ctx, id, deletedBy)
	}
	return nil
}
func (m *flightDataServiceMock) GetByID(ctx context.Context, id []byte) (*flightdata.FlightData, error) {
	if m.getByIDFn != nil {
		return m.getByIDFn(ctx, id)
	}
	return nil, nil
}
func (m *flightDataServiceMock) GetByFlightID(ctx context.Context, id []byte) (*flightdata.FlightData, error) {
	if m.getByFlightFn != nil {
		return m.getByFlightFn(ctx, id)
	}
	return nil, nil
}
func (m *flightDataServiceMock) GetByMissionID(ctx context.Context, id []byte) (*flightdata.FlightData, error) {
	if m.getByMisFn != nil {
		return m.getByMisFn(ctx, id)
	}
	return nil, nil
}
func (m *flightDataServiceMock) ListByMissionID(ctx context.Context, missionID []byte) ([]flightdata.FlightData, error) {
	if m.listFn != nil {
		rows, _, err := m.listFn(ctx, "", missionID, nil, 0, 0)
		return rows, err
	}
	return nil, nil
}
func (m *flightDataServiceMock) List(ctx context.Context, date string, missionID, flightDataID []byte, limit, offset int) ([]flightdata.FlightData, int64, error) {
	if m.listFn != nil {
		return m.listFn(ctx, date, missionID, flightDataID, limit, offset)
	}
	return nil, 0, nil
}
func (m *flightDataServiceMock) GetSPODetailsByFlightDataID(ctx context.Context, id []byte) (*flightdata.SPODetails, error) {
	if m.getSpoFn != nil {
		return m.getSpoFn(ctx, id)
	}
	return nil, nil
}

func (m *flightDataServiceMock) UpsertSPODetails(context.Context, []byte, *flightdata.SPOUpsertData) error {
	return nil
}

func (m *userServiceMock) GetByID(ctx context.Context, id []byte) (*userdomain.User, error) {
	if m.getByIDFn != nil {
		return m.getByIDFn(ctx, id)
	}
	return nil, nil
}

func (m *facilityServiceMock) GetByID(ctx context.Context, id []byte) (*facilitydomain.Facility, error) {
	if m.getByIDFn != nil {
		return m.getByIDFn(ctx, id)
	}
	return nil, nil
}

func (m *flightDataMissionServiceMock) GetByID(ctx context.Context, id []byte) (*missiondomain.Mission, error) {
	if m.getByIDFn != nil {
		return m.getByIDFn(ctx, id)
	}
	return nil, nil
}

func (m *hospitalServiceMock) GetByID(ctx context.Context, id []byte) (*hospitaldomain.Hospital, error) {
	if m.getByIDFn != nil {
		return m.getByIDFn(ctx, id)
	}
	return nil, nil
}

func (m *icaoServiceMock) GetByID(ctx context.Context, id []byte) (*icaodomain.ICAO, error) {
	if m.getByIDFn != nil {
		return m.getByIDFn(ctx, id)
	}
	return nil, nil
}

func newFlightDataApp(h *FlightDataHandler) *fiber.App {
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_id", uuidv7.MustBytes())
		return c.Next()
	})
	app.Post("/flight-data", h.Create)
	app.Get("/flight-data/get-all", h.List)
	app.Get("/flight-data/get-all/dt", h.ListDatatable)
	app.Get("/flight-data/:id", h.Get)
	app.Patch("/flight-data/:id", h.Update)
	app.Delete("/flight-data/:id", h.Delete)
	return app
}

func doReqFD(t *testing.T, app *fiber.App, method, path string, body []byte) (int, string) {
	t.Helper()
	var rdr io.Reader
	if body != nil {
		rdr = bytes.NewReader(body)
	}
	req, _ := http.NewRequest(method, path, rdr)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	return resp.StatusCode, string(b)
}

func TestFlightDataHandler(t *testing.T) {
	t.Run("new", func(t *testing.T) {
		h := NewFlightDataHandler(&flightDataServiceMock{})
		if h == nil || h.validate == nil {
			t.Fatal("expected handler initialized")
		}
	})

	t.Run("create parse and validation errors", func(t *testing.T) {
		h := NewFlightDataHandler(&flightDataServiceMock{})
		code, _ := doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(`{"data":`))
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}
		code, _ = doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(`{}`))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		body := `{"data":{"type":"flight_data","attributes":{"mission_id":"bad","co_pilot_id":"bad"}}}`
		code, _ = doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		body = `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"bad"}}}`
		code, _ = doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
	})

	t.Run("create optional and datetime validation", func(t *testing.T) {
		h := NewFlightDataHandler(&flightDataServiceMock{})
		body := `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","from_icao_id":"bad"}}}`
		code, _ := doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		body = `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","flight_take_off":"bad"}}}`
		code, _ = doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		body = `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","from_hospital_id":"bad"}}}`
		code, _ = doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		body = `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","to_icao_id":"bad"}}}`
		code, _ = doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		body = `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","to_hospital_id":"bad"}}}`
		code, _ = doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		body = `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","flight_landing":"bad"}}}`
		code, _ = doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		hRef := NewFlightDataHandler(&flightDataServiceMock{}).
			WithUserService(&userServiceMock{getByIDFn: func(context.Context, []byte) (*userdomain.User, error) {
				return &userdomain.User{ID: uuidv7.MustBytes()}, nil
			}}).
			WithHospitalService(&hospitalServiceMock{getByIDFn: func(context.Context, []byte) (*hospitaldomain.Hospital, error) {
				return nil, nil
			}}).
			WithICAOService(&icaoServiceMock{getByIDFn: func(context.Context, []byte) (*icaodomain.ICAO, error) {
				return &icaodomain.ICAO{ID: uuidv7.MustBytes()}, nil
			}})
		body = `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","from_hospital_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}}}`
		code, _ = doReqFD(t, newFlightDataApp(hRef), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
	})

	t.Run("create allows omitting co_pilot_id", func(t *testing.T) {
		var gotPilotID []byte
		h := NewFlightDataHandler(&flightDataServiceMock{
			createFn: func(_ context.Context, row *flightdata.FlightData) error {
				gotPilotID = append([]byte(nil), row.CoPilotID...)
				row.ID = uuidv7.MustBytes()
				row.CreatedAt = time.Now().UTC()
				row.UpdatedAt = row.CreatedAt
				return nil
			},
			getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
				return &flightdata.FlightData{ID: id, MissionID: uuidv7.MustBytes(), CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}, nil
			},
		})
		body := `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}}}`
		code, _ := doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusCreated {
			t.Fatalf("expected 201 got %d", code)
		}
		if len(gotPilotID) != 0 {
			t.Fatalf("expected empty pilot_id when co_pilot_id is omitted, got %x", gotPilotID)
		}
	})

	t.Run("create service errors and success fallback", func(t *testing.T) {
		h1 := NewFlightDataHandler(&flightDataServiceMock{
			createFn: func(context.Context, *flightdata.FlightData) error { return flightdata.ErrOriginLocationReference },
		})
		body := `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}}}`
		code, _ := doReqFD(t, newFlightDataApp(h1), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		body = `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","from_hospital_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}}}`
		h1Hospital := NewFlightDataHandler(&flightDataServiceMock{
			createFn: func(context.Context, *flightdata.FlightData) error { return flightdata.ErrOriginLocationReference },
		})
		code, respBody := doReqFD(t, newFlightDataApp(h1Hospital), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		if !strings.Contains(respBody, `"pointer":"/data/attributes/from_hospital_id"`) {
			t.Fatalf("expected origin hospital pointer, got %s", respBody)
		}
		h1Missing := NewFlightDataHandler(&flightDataServiceMock{}).
			WithHospitalService(&hospitalServiceMock{getByIDFn: func(context.Context, []byte) (*hospitaldomain.Hospital, error) {
				return nil, gorm.ErrRecordNotFound
			}})
		body = `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","from_hospital_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}}}`
		code, respBody = doReqFD(t, newFlightDataApp(h1Missing), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		if !strings.Contains(respBody, `"pointer":"/data/attributes/from_hospital_id"`) {
			t.Fatalf("expected record-not-found origin hospital pointer, got %s", respBody)
		}
		hMission := NewFlightDataHandler(&flightDataServiceMock{}).
			WithMissionService(&flightDataMissionServiceMock{getByIDFn: func(context.Context, []byte) (*missiondomain.Mission, error) {
				return nil, gorm.ErrRecordNotFound
			}})
		body = `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}}}`
		code, respBody = doReqFD(t, newFlightDataApp(hMission), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		if !strings.Contains(respBody, `"pointer":"/data/attributes/mission_id"`) {
			t.Fatalf("expected mission pointer, got %s", respBody)
		}
		h2 := NewFlightDataHandler(&flightDataServiceMock{
			createFn: func(context.Context, *flightdata.FlightData) error { return errors.New("x") },
		})
		code, _ = doReqFD(t, newFlightDataApp(h2), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}
		h3 := NewFlightDataHandler(&flightDataServiceMock{
			createFn: func(_ context.Context, row *flightdata.FlightData) error {
				row.ID = uuidv7.MustBytes()
				row.CreatedAt = time.Now().UTC()
				row.UpdatedAt = row.CreatedAt
				return nil
			},
			getByIDFn: func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, errors.New("no") },
		})
		code, _ = doReqFD(t, newFlightDataApp(h3), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusCreated {
			t.Fatalf("expected 201 got %d", code)
		}
		hSuccessCreated := NewFlightDataHandler(&flightDataServiceMock{
			createFn: func(_ context.Context, row *flightdata.FlightData) error {
				row.ID = uuidv7.MustBytes()
				row.CreatedAt = time.Now().UTC()
				row.UpdatedAt = row.CreatedAt
				return nil
			},
			getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
				return &flightdata.FlightData{ID: id, MissionID: uuidv7.MustBytes(), CoPilotID: uuidv7.MustBytes(), CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}, nil
			},
		})
		code, _ = doReqFD(t, newFlightDataApp(hSuccessCreated), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusCreated {
			t.Fatalf("expected 201 got %d", code)
		}

		h4 := NewFlightDataHandler(&flightDataServiceMock{
			createFn: func(_ context.Context, row *flightdata.FlightData) error {
				row.ID = uuidv7.MustBytes()
				row.CreatedAt = time.Now().UTC()
				row.UpdatedAt = row.CreatedAt
				return nil
			},
			getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
				return &flightdata.FlightData{
					ID:        id,
					MissionID: uuidv7.MustBytes(),
					CoPilotID: uuidv7.MustBytes(),
					Mission:   &missiondomain.Mission{Type: "SPO"},
					CreatedAt: time.Now().UTC(),
					UpdatedAt: time.Now().UTC(),
				}, nil
			},
			getSpoFn: func(context.Context, []byte) (*flightdata.SPODetails, error) { return nil, errors.New("spo err") },
		})
		code, _ = doReqFD(t, newFlightDataApp(h4), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}
		h5 := NewFlightDataHandler(&flightDataServiceMock{
			createFn: func(_ context.Context, row *flightdata.FlightData) error {
				row.ID = uuidv7.MustBytes()
				row.CreatedAt = time.Now().UTC()
				row.UpdatedAt = row.CreatedAt
				row.Mission = &missiondomain.Mission{Type: "SPO"}
				return nil
			},
			getByIDFn: func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, errors.New("x") },
			getSpoFn: func(context.Context, []byte) (*flightdata.SPODetails, error) {
				return nil, errors.New("spo fallback err")
			},
		})
		code, _ = doReqFD(t, newFlightDataApp(h5), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}
	})

	t.Run("get flows", func(t *testing.T) {
		h := NewFlightDataHandler(&flightDataServiceMock{})
		code, _ := doReqFD(t, newFlightDataApp(h), http.MethodGet, "/flight-data/bad", nil)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}

		id := mustUUIDStringHospital(t, uuidv7.MustBytes())
		hErr := NewFlightDataHandler(&flightDataServiceMock{
			getByMisFn: func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, errors.New("x") },
		})
		code, _ = doReqFD(t, newFlightDataApp(hErr), http.MethodGet, "/flight-data/"+id, nil)
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}

		hNot := NewFlightDataHandler(&flightDataServiceMock{
			getByMisFn:    func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, nil },
			getByIDFn:     func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, nil },
			getByFlightFn: func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, nil },
		})
		code, _ = doReqFD(t, newFlightDataApp(hNot), http.MethodGet, "/flight-data/"+id, nil)
		if code != http.StatusNotFound {
			t.Fatalf("expected 404 got %d", code)
		}

		row := &flightdata.FlightData{
			ID:         uuidv7.MustBytes(),
			MissionID:  uuidv7.MustBytes(),
			CoPilotID:  uuidv7.MustBytes(),
			Mission:    &missiondomain.Mission{ID: uuidv7.MustBytes(), Type: "SPO"},
			CoPilot:    &userdomain.User{ID: uuidv7.MustBytes(), FirstName: "A", LastName: "B", Email: "a@b.c"},
			FromICAO:   &icaodomain.ICAO{ID: uuidv7.MustBytes(), ICAOCode: "WIII"},
			ToHospital: &hospitaldomain.Hospital{ID: uuidv7.MustBytes(), Name: "RS"},
			CreatedAt:  time.Now().UTC(),
			UpdatedAt:  time.Now().UTC(),
		}
		hOK := NewFlightDataHandler(&flightDataServiceMock{
			getByMisFn: func(context.Context, []byte) (*flightdata.FlightData, error) { return row, nil },
			getSpoFn:   func(context.Context, []byte) (*flightdata.SPODetails, error) { return &flightdata.SPODetails{}, nil },
		})
		code, body := doReqFD(t, newFlightDataApp(hOK), http.MethodGet, "/flight-data/"+id, nil)
		if code != http.StatusOK || !strings.Contains(body, `"flight_data"`) {
			t.Fatalf("expected 200 got %d body=%s", code, body)
		}

		hByIDErr := NewFlightDataHandler(&flightDataServiceMock{
			getByMisFn: func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, nil },
			getByIDFn:  func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, errors.New("x") },
		})
		code, _ = doReqFD(t, newFlightDataApp(hByIDErr), http.MethodGet, "/flight-data/"+id, nil)
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}
		hByFlightErr := NewFlightDataHandler(&flightDataServiceMock{
			getByMisFn:    func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, nil },
			getByIDFn:     func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, nil },
			getByFlightFn: func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, errors.New("x") },
		})
		code, _ = doReqFD(t, newFlightDataApp(hByFlightErr), http.MethodGet, "/flight-data/"+id, nil)
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}
		hBuildErr := NewFlightDataHandler(&flightDataServiceMock{
			getByMisFn: func(context.Context, []byte) (*flightdata.FlightData, error) {
				return &flightdata.FlightData{ID: uuidv7.MustBytes(), Mission: &missiondomain.Mission{Type: "SPO"}, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}, nil
			},
			getSpoFn: func(context.Context, []byte) (*flightdata.SPODetails, error) { return nil, errors.New("x") },
		})
		code, _ = doReqFD(t, newFlightDataApp(hBuildErr), http.MethodGet, "/flight-data/"+id, nil)
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}
	})

	t.Run("update and delete flows", func(t *testing.T) {
		id := mustUUIDStringHospital(t, uuidv7.MustBytes())
		body := `{"data":{"type":"flight_data_update","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}}}`
		refBody := `{"data":{"type":"flight_data_update","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","from_hospital_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}}}`

		hUpdateBadID := NewFlightDataHandler(&flightDataServiceMock{})
		code, _ := doReqFD(t, newFlightDataApp(hUpdateBadID), http.MethodPatch, "/flight-data/bad", []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}

		hUpdateValidate := NewFlightDataHandler(&flightDataServiceMock{})
		code, _ = doReqFD(t, newFlightDataApp(hUpdateValidate), http.MethodPatch, "/flight-data/"+id, []byte(`{}`))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		noPilotBody := `{"data":{"type":"flight_data_update","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}}}`
		hUpdateNoPilot := NewFlightDataHandler(&flightDataServiceMock{
			updateFn: func(_ context.Context, row *flightdata.FlightData) error {
				if len(row.CoPilotID) != 16 {
					t.Fatalf("expected existing pilot_id to stay unchanged, got %d bytes", len(row.CoPilotID))
				}
				row.UpdatedAt = time.Now().UTC()
				return nil
			},
			getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
				now := time.Now().UTC()
				return &flightdata.FlightData{ID: id, MissionID: uuidv7.MustBytes(), CoPilotID: uuidv7.MustBytes(), CreatedAt: now, UpdatedAt: now}, nil
			},
		})
		code, _ = doReqFD(t, newFlightDataApp(hUpdateNoPilot), http.MethodPatch, "/flight-data/"+id, []byte(noPilotBody))
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
		hUpdateRef := NewFlightDataHandler(&flightDataServiceMock{}).
			WithUserService(&userServiceMock{getByIDFn: func(context.Context, []byte) (*userdomain.User, error) {
				return &userdomain.User{ID: uuidv7.MustBytes()}, nil
			}}).
			WithHospitalService(&hospitalServiceMock{getByIDFn: func(context.Context, []byte) (*hospitaldomain.Hospital, error) {
				return nil, nil
			}}).
			WithICAOService(&icaoServiceMock{getByIDFn: func(context.Context, []byte) (*icaodomain.ICAO, error) {
				return &icaodomain.ICAO{ID: uuidv7.MustBytes()}, nil
			}})
		code, _ = doReqFD(t, newFlightDataApp(hUpdateRef), http.MethodPatch, "/flight-data/"+id, []byte(refBody))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}

		spoBody := `{"data":{"type":"flight_data_update","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","logging":{"slings":[{"sling_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}]}}}}`
		hUpdateSPORef := NewFlightDataHandler(&flightDataServiceMock{
			updateFn: func(_ context.Context, row *flightdata.FlightData) error {
				row.UpdatedAt = time.Now().UTC()
				return nil
			},
			getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
				now := time.Now().UTC()
				return &flightdata.FlightData{ID: id, MissionID: uuidv7.MustBytes(), CoPilotID: uuidv7.MustBytes(), CreatedAt: now, UpdatedAt: now}, nil
			},
		}).WithFacilityService(&facilityServiceMock{
			getByIDFn: func(context.Context, []byte) (*facilitydomain.Facility, error) {
				return nil, gorm.ErrRecordNotFound
			},
		})
		code, respBody := doReqFD(t, newFlightDataApp(hUpdateSPORef), http.MethodPatch, "/flight-data/"+id, []byte(spoBody))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		if !strings.Contains(respBody, `"pointer":"/data/attributes/logging/slings/0/sling_id"`) {
			t.Fatalf("expected logging sling pointer, got %s", respBody)
		}

		hUpdateSPOWrongCategory := NewFlightDataHandler(&flightDataServiceMock{
			updateFn: func(_ context.Context, row *flightdata.FlightData) error {
				row.UpdatedAt = time.Now().UTC()
				return nil
			},
			getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
				now := time.Now().UTC()
				return &flightdata.FlightData{ID: id, MissionID: uuidv7.MustBytes(), CoPilotID: uuidv7.MustBytes(), CreatedAt: now, UpdatedAt: now}, nil
			},
		}).WithFacilityService(&facilityServiceMock{
			getByIDFn: func(context.Context, []byte) (*facilitydomain.Facility, error) {
				return &facilitydomain.Facility{ID: uuidv7.MustBytes(), Category: "wrong-category"}, nil
			},
		})
		code, _ = doReqFD(t, newFlightDataApp(hUpdateSPOWrongCategory), http.MethodPatch, "/flight-data/"+id, []byte(spoBody))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}

		hUpdateErr := NewFlightDataHandler(&flightDataServiceMock{
			updateFn: func(context.Context, *flightdata.FlightData) error { return errors.New("x") },
			getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
				now := time.Now().UTC()
				return &flightdata.FlightData{ID: id, MissionID: uuidv7.MustBytes(), CoPilotID: uuidv7.MustBytes(), CreatedAt: now, UpdatedAt: now}, nil
			},
		})
		code, _ = doReqFD(t, newFlightDataApp(hUpdateErr), http.MethodPatch, "/flight-data/"+id, []byte(body))
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}

		hUpdateFKErr := NewFlightDataHandler(&flightDataServiceMock{
			updateFn: func(context.Context, *flightdata.FlightData) error {
				return &mysqldriver.MySQLError{
					Number:  1452,
					Message: "Cannot add or update a child row: a foreign key constraint fails (`wucher`.`flight_data`, CONSTRAINT `fk_flight_data_from_hospital` FOREIGN KEY (`from_hospital_id`) REFERENCES `hospitals` (`id`) ON DELETE SET NULL ON UPDATE CASCADE)",
				}
			},
			getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
				now := time.Now().UTC()
				return &flightdata.FlightData{ID: id, MissionID: uuidv7.MustBytes(), CoPilotID: uuidv7.MustBytes(), CreatedAt: now, UpdatedAt: now}, nil
			},
		})
		code, _ = doReqFD(t, newFlightDataApp(hUpdateFKErr), http.MethodPatch, "/flight-data/"+id, []byte(body))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}

		hUpdateOK := NewFlightDataHandler(&flightDataServiceMock{
			updateFn: func(_ context.Context, row *flightdata.FlightData) error {
				if row.CreatedAt.IsZero() {
					t.Fatal("expected created_at preserved on update")
				}
				row.UpdatedAt = time.Now().UTC()
				return nil
			},
			getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
				now := time.Now().UTC()
				return &flightdata.FlightData{ID: id, MissionID: uuidv7.MustBytes(), CoPilotID: uuidv7.MustBytes(), CreatedAt: now, UpdatedAt: now}, nil
			},
		})
		code, _ = doReqFD(t, newFlightDataApp(hUpdateOK), http.MethodPatch, "/flight-data/"+id, []byte(body))
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}

		hDeleteBadID := NewFlightDataHandler(&flightDataServiceMock{})
		code, _ = doReqFD(t, newFlightDataApp(hDeleteBadID), http.MethodDelete, "/flight-data/bad", nil)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}

		hDeleteErr := NewFlightDataHandler(&flightDataServiceMock{
			deleteFn: func(context.Context, []byte, []byte) error { return errors.New("x") },
		})
		code, _ = doReqFD(t, newFlightDataApp(hDeleteErr), http.MethodDelete, "/flight-data/"+id, nil)
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}

		hDeleteOK := NewFlightDataHandler(&flightDataServiceMock{
			deleteFn: func(context.Context, []byte, []byte) error { return nil },
		})
		code, bodyResp := doReqFD(t, newFlightDataApp(hDeleteOK), http.MethodDelete, "/flight-data/"+id, nil)
		if code != http.StatusOK || !strings.Contains(bodyResp, `"deleted":true`) {
			t.Fatalf("expected 200 deleted response got %d body=%s", code, bodyResp)
		}
	})

	t.Run("helpers and mappings", func(t *testing.T) {
		if r := missionRefFlightData(nil); r != nil {
			t.Fatal("expected nil")
		}
		if r := coPilotRefFlightData(nil); r != nil {
			t.Fatal("expected nil")
		}
		if r := fromLocationRefFlightData(nil); r != nil {
			t.Fatal("expected nil")
		}
		if r := toLocationRefFlightData(nil); r != nil {
			t.Fatal("expected nil")
		}
		r1 := missionRefFlightData(&flightdata.FlightData{MissionID: uuidv7.MustBytes()})
		if r1 == nil || r1.ID == "" {
			t.Fatal("expected mission id ref")
		}
		r2 := coPilotRefFlightData(&flightdata.FlightData{CoPilotID: uuidv7.MustBytes()})
		if r2 == nil || r2.ID == "" {
			t.Fatal("expected pilot id ref")
		}
		if missionRefFlightData(&flightdata.FlightData{}) != nil {
			t.Fatal("expected nil ref for empty")
		}
		fromHosp := fromLocationRefFlightData(&flightdata.FlightData{FromHospital: &hospitaldomain.Hospital{ID: uuidv7.MustBytes(), Name: " x "}})
		if fromHosp == nil || fromHosp.Name != "x" {
			t.Fatal("expected hospital from")
		}
		fromID := fromLocationRefFlightData(&flightdata.FlightData{FromICAOID: uuidv7.MustBytes()})
		if fromID == nil || fromID.ID == "" {
			t.Fatal("expected from id")
		}
		fromID2 := fromLocationRefFlightData(&flightdata.FlightData{FromHospitalID: uuidv7.MustBytes()})
		if fromID2 == nil || fromID2.ID == "" {
			t.Fatal("expected from hospital id")
		}
		toIcao := toLocationRefFlightData(&flightdata.FlightData{ToICAO: &icaodomain.ICAO{ID: uuidv7.MustBytes(), ICAOCode: " WIII "}})
		if toIcao == nil || toIcao.Code != "WIII" {
			t.Fatal("expected to icao")
		}
		toID := toLocationRefFlightData(&flightdata.FlightData{ToICAOID: uuidv7.MustBytes()})
		if toID == nil || toID.ID == "" {
			t.Fatal("expected to id")
		}
		toID2 := toLocationRefFlightData(&flightdata.FlightData{ToHospitalID: uuidv7.MustBytes()})
		if toID2 == nil || toID2.ID == "" {
			t.Fatal("expected to hospital id")
		}

		if _, errObj := parseRequiredUUID(" ", "/x"); errObj == nil {
			t.Fatal("expected required err")
		}
		if _, errObj := parseRequiredUUID("bad", "/x"); errObj == nil {
			t.Fatal("expected invalid uuid err")
		}
		if id, errObj := parseRequiredUUID(mustUUIDStringHospital(t, uuidv7.MustBytes()), "/x"); errObj != nil || len(id) != 16 {
			t.Fatal("expected valid id")
		}
		if _, errObj := parseOptionalUUIDFlightData(nil, "/x"); errObj != nil {
			t.Fatal("expected nil err")
		}
		raw := " "
		if id, errObj := parseOptionalUUIDFlightData(&raw, "/x"); errObj != nil || id != nil {
			t.Fatal("expected nil")
		}
		raw = "bad"
		if _, errObj := parseOptionalUUIDFlightData(&raw, "/x"); errObj == nil {
			t.Fatal("expected err")
		}
		raw = mustUUIDStringHospital(t, uuidv7.MustBytes())
		if id, errObj := parseOptionalUUIDFlightData(&raw, "/x"); errObj != nil || len(id) != 16 {
			t.Fatal("expected valid optional uuid")
		}
		if _, errObj := parseRFC3339("bad", "/x"); errObj == nil {
			t.Fatal("expected err")
		}
		if _, errObj := parseRFC3339("2026-04-20T08:00:00Z", "/x"); errObj != nil {
			t.Fatal("expected ok")
		}

		app := fiber.New()
		_ = app.Get("/", func(c *fiber.Ctx) error {
			if err := writeFlightDataServiceError(c, flightdata.ErrDestinationLocationReference); err != nil {
				return err
			}
			return nil
		})
		code, _ := doReqFD(t, app, http.MethodGet, "/", nil)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		app2 := fiber.New()
		_ = app2.Get("/", func(c *fiber.Ctx) error { return writeFlightDataServiceError(c, flightdata.ErrRequired) })
		code, _ = doReqFD(t, app2, http.MethodGet, "/", nil)
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}

		app3 := fiber.New()
		_ = app3.Get("/", func(c *fiber.Ctx) error {
			return writeFlightDataServiceError(c, errors.Join(
				errors.New("wrapped service error"),
				&mysqldriver.MySQLError{
					Number:  1452,
					Message: "Cannot add or update a child row: a foreign key constraint fails (`wucher`.`flight_data`, CONSTRAINT `fk_flight_data_to_hospital` FOREIGN KEY (`to_hospital_id`) REFERENCES `hospitals` (`id`) ON DELETE SET NULL ON UPDATE CASCADE)",
				},
			))
		})
		code, body := doReqFD(t, app3, http.MethodGet, "/", nil)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d body=%s", code, body)
		}
		if !strings.Contains(body, `"pointer":"/data/attributes/to_hospital_id"`) {
			t.Fatalf("expected mapped destination pointer, got %s", body)
		}

		svc := &flightDataServiceMock{
			getSpoFn: func(context.Context, []byte) (*flightdata.SPODetails, error) {
				flightID := uuidv7.MustBytes()
				slingID := uuidv7.MustBytes()
				return &flightdata.SPODetails{
					HESLO: flightdata.SPOHESLODetails{
						Flights: []flightdata.SPOHESLOFlight{{ID: flightID, FuelTruckID: uuidv7.MustBytes(), FuelTruckName: " a ", LoggingSlingID: uuidv7.MustBytes(), LoggingSlingName: " b "}},
						Slings:  []flightdata.SPOHESLOSling{{ID: uuidv7.MustBytes(), HESLOFlightID: flightID, SlingID: slingID, SlingName: " c "}},
					},
					Logging: flightdata.SPOLoggingDetails{
						Slings: []flightdata.SPOLoggingSling{{ID: uuidv7.MustBytes(), SlingID: uuidv7.MustBytes(), SlingName: " d "}},
					},
					HEC: flightdata.SPOHECDetails{
						Flights: []flightdata.SPOHECFlight{{ID: uuidv7.MustBytes(), EquipmentID: uuidv7.MustBytes(), EquipmentName: " e "}},
						Slings:  []flightdata.SPOHECSling{{ID: uuidv7.MustBytes(), SlingID: uuidv7.MustBytes(), SlingName: " f "}},
						Loads:   []flightdata.SPOHECLoad{{ID: uuidv7.MustBytes(), LoadCategory: " g ", Quantity: 1}},
					},
				}, nil
			},
		}
		h := NewFlightDataHandler(svc)
		app4 := fiber.New()
		_ = app4.Get("/", func(c *fiber.Ctx) error {
			row := &flightdata.FlightData{ID: uuidv7.MustBytes(), Mission: &missiondomain.Mission{Type: "SPO"}, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}
			heslo, _, _, err := h.buildSPODetails(c, row)
			if err != nil {
				return err
			}
			if heslo == nil || len(heslo.Flights) != 1 || len(heslo.Flights[0].Slings) != 1 {
				return errors.New("unexpected heslo response shape")
			}
			_, _, _, _ = h.buildSPODetails(c, &flightdata.FlightData{})
			_, _, _, _ = h.buildSPODetails(c, &flightdata.FlightData{ID: []byte{1}, Mission: &missiondomain.Mission{Type: "SPO"}})
			return c.SendStatus(http.StatusOK)
		})
		code, _ = doReqFD(t, app4, http.MethodGet, "/", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
	})

	t.Run("create full optional coverage", func(t *testing.T) {
		h := NewFlightDataHandler(&flightDataServiceMock{
			createFn: func(_ context.Context, row *flightdata.FlightData) error {
				row.ID = uuidv7.MustBytes()
				row.CreatedAt = time.Now().UTC()
				row.UpdatedAt = row.CreatedAt
				return nil
			},
			getByIDFn: func(context.Context, []byte) (*flightdata.FlightData, error) { return nil, errors.New("x") },
		})
		body := `{"data":{"type":"flight_data","attributes":{"mission_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","co_pilot_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","flight_take_off":"2026-04-20T08:00:00Z","flight_landing":"2026-04-20T09:00:00Z","flight_duration_seconds":3600,"flight_red_seconds":10,"max_n1":1.2,"max_n2":2.3,"pax_count":4,"landing_count":2,"rotor_brake_cycle":3,"hook_releases":5,"ticket_no":" A ","engine":" B ","delivery_note_number":" C ","customer_name":" D ","flight_plan_distance":1.2,"flight_plan_time_seconds":100,"flight_plan_true_course":2.4,"fuel_before_flight":1.1,"fuel_upload":2.2,"fuel_after_flight":3.3,"fuel_planning":4.4,"other_information":" E "}}}`
		code, _ := doReqFD(t, newFlightDataApp(h), http.MethodPost, "/flight-data", []byte(body))
		if code != http.StatusCreated {
			t.Fatalf("expected 201 got %d", code)
		}
	})

	t.Run("list and datatable", func(t *testing.T) {
		now := time.Date(2026, 6, 4, 10, 44, 36, 0, time.UTC)
		rowID := uuidv7.MustBytes()
		h := NewFlightDataHandler(&flightDataServiceMock{
			listFn: func(context.Context, string, []byte, []byte, int, int) ([]flightdata.FlightData, int64, error) {
				return []flightdata.FlightData{{
					ID:        rowID,
					MissionID: uuidv7.MustBytes(),
					CoPilotID: uuidv7.MustBytes(),
					CreatedAt: now,
					UpdatedAt: now,
				}}, 1, nil
			},
		})
		code, body := doReqFD(t, newFlightDataApp(h), http.MethodGet, "/flight-data/get-all?date=2026-06-04", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d body=%s", code, body)
		}
		if !strings.Contains(body, `"total":1`) {
			t.Fatalf("expected total meta, got %s", body)
		}
		if !strings.Contains(body, `"type":"flight_data"`) {
			t.Fatalf("expected flight_data item, got %s", body)
		}

		code, body = doReqFD(t, newFlightDataApp(h), http.MethodGet, "/flight-data/get-all/dt?date=bad", nil)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d body=%s", code, body)
		}
	})
}
