package handlers

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

	"github.com/gofiber/fiber/v2"

	afterflightinspection "wucher/internal/domain/after_flight_inspection"
	"wucher/internal/domain/flight"
	flightdata "wucher/internal/domain/flight_data"
	flightinspection "wucher/internal/domain/flight_inspection"
	"wucher/internal/domain/mission"
	reserveac "wucher/internal/domain/reserve_ac"
	takeoverdomain "wucher/internal/domain/takeover"
	"wucher/internal/shared/pkg/uuidv7"
)

type flightServiceMock struct {
	createFn       func(context.Context, *flight.Flight) error
	getByIDFn      func(context.Context, []byte) (*flight.Flight, error)
	listByUserIDFn func(context.Context, []byte, int, int) ([]flight.Flight, int64, error)
	listFn         func(context.Context, string, string, string, int, int) ([]flight.Flight, int64, error)
}

type missionServiceMock struct {
	listByFlightIDFn  func(context.Context, []byte) ([]mission.Mission, error)
	listByFlightIDsFn func(context.Context, [][]byte) ([]mission.Mission, error)
	listFn            func(context.Context, string, string, []byte, int, int) ([]mission.Mission, int64, error)
	listDTFn          func(context.Context, mission.ListFilter, int, int) ([]mission.Mission, int64, int64, error)
	deleteByIDFn      func(context.Context, []byte, []byte) error
}

type afterFlightInspectionServiceMock struct {
	getByFlightInspectionIDFn func(context.Context, []byte) (*afterflightinspection.AfterFlightInspection, error)
}

type takeoverServiceMock struct {
	getByIDFn func(context.Context, []byte) (*takeoverdomain.TakeoverAc, error)
}

func (m *flightServiceMock) Create(ctx context.Context, row *flight.Flight) error {
	if m.createFn != nil {
		return m.createFn(ctx, row)
	}
	return nil
}
func (m *flightServiceMock) Update(context.Context, *flight.Flight) error { return nil }
func (m *flightServiceMock) Delete(context.Context, []byte, []byte) error { return nil }
func (m *flightServiceMock) GetByID(ctx context.Context, id []byte) (*flight.Flight, error) {
	if m.getByIDFn != nil {
		return m.getByIDFn(ctx, id)
	}
	return nil, nil
}
func (m *flightServiceMock) ListByUserID(ctx context.Context, userID []byte, limit, offset int) ([]flight.Flight, int64, error) {
	if m.listByUserIDFn != nil {
		return m.listByUserIDFn(ctx, userID, limit, offset)
	}
	return nil, 0, nil
}
func (m *flightServiceMock) ListByCreatedBy(context.Context, []byte, string, string, string, int, int) ([]flight.Flight, int64, error) {
	return nil, 0, nil
}
func (m *flightServiceMock) GetByReserveAcID(context.Context, []byte) (*flight.Flight, error) {
	return nil, nil
}
func (m *flightServiceMock) ListByReserveAcID(context.Context, []byte) ([]flight.Flight, error) {
	return nil, nil
}
func (m *flightServiceMock) GetByTakeoverAcID(context.Context, []byte) (*flight.Flight, error) {
	return nil, nil
}
func (m *flightServiceMock) ListByTakeoverAcIDs(context.Context, [][]byte) ([]flight.Flight, error) {
	return nil, nil
}
func (m *flightServiceMock) GetByDutyRosterID(context.Context, []byte) (*flight.Flight, error) {
	return nil, nil
}
func (m *flightServiceMock) ListByDutyRosterIDs(context.Context, [][]byte) ([]flight.Flight, error) {
	return nil, nil
}
func (m *flightServiceMock) List(ctx context.Context, filter string, date string, sort string, limit, offset int) ([]flight.Flight, int64, error) {
	if m.listFn != nil {
		return m.listFn(ctx, filter, date, sort, limit, offset)
	}
	return nil, 0, nil
}

func (m *missionServiceMock) Create(context.Context, *mission.Mission) error { return nil }
func (m *missionServiceMock) CreateType(context.Context, *mission.MissionCategory) error {
	return nil
}
func (m *missionServiceMock) UpdateByID(context.Context, []byte, string, []byte, string, []byte) error {
	return nil
}
func (m *missionServiceMock) DeleteByFlightID(context.Context, []byte, []byte) error { return nil }
func (m *missionServiceMock) AttachFile(context.Context, []byte, []byte) error       { return nil }
func (m *missionServiceMock) DetachFile(context.Context, []byte, []byte) error       { return nil }
func (m *missionServiceMock) DeleteByID(ctx context.Context, missionID []byte, deletedBy []byte) error {
	if m.deleteByIDFn != nil {
		return m.deleteByIDFn(ctx, missionID, deletedBy)
	}
	return nil
}
func (m *missionServiceMock) ListByFlightID(ctx context.Context, flightID []byte) ([]mission.Mission, error) {
	if m.listByFlightIDFn != nil {
		return m.listByFlightIDFn(ctx, flightID)
	}
	return nil, nil
}
func (m *missionServiceMock) ListByFlightIDs(ctx context.Context, flightIDs [][]byte) ([]mission.Mission, error) {
	if m.listByFlightIDsFn != nil {
		return m.listByFlightIDsFn(ctx, flightIDs)
	}
	return nil, nil
}
func (m *missionServiceMock) GetByID(context.Context, []byte) (*mission.Mission, error) {
	return nil, nil
}
func (m *missionServiceMock) GetByFlightID(context.Context, []byte) (*mission.Mission, error) {
	return nil, nil
}
func (m *missionServiceMock) List(ctx context.Context, filter string, sort string, flightID []byte, flightDataStatus string, limit, offset int) ([]mission.Mission, int64, error) {
	if m.listFn != nil {
		return m.listFn(ctx, filter, sort, flightID, limit, offset)
	}
	return nil, 0, nil
}
func (m *missionServiceMock) ListDatatable(ctx context.Context, filter mission.ListFilter, limit, offset int) ([]mission.Mission, int64, int64, error) {
	if m.listDTFn != nil {
		return m.listDTFn(ctx, filter, limit, offset)
	}
	return nil, 0, 0, nil
}
func (m *missionServiceMock) ListTypes(context.Context) ([]mission.MissionCategory, error) {
	return nil, nil
}

func (m *afterFlightInspectionServiceMock) Upsert(context.Context, []byte, *afterflightinspection.UpsertRequest) (*afterflightinspection.AfterFlightInspection, error) {
	return nil, nil
}
func (m *afterFlightInspectionServiceMock) GetByFlightInspectionID(ctx context.Context, id []byte) (*afterflightinspection.AfterFlightInspection, error) {
	if m.getByFlightInspectionIDFn != nil {
		return m.getByFlightInspectionIDFn(ctx, id)
	}
	return nil, nil
}
func (m *afterFlightInspectionServiceMock) DeleteByFlightInspectionID(context.Context, []byte) error {
	return nil
}

func (m *takeoverServiceMock) Create(context.Context, *takeoverdomain.TakeoverAc) error { return nil }
func (m *takeoverServiceMock) Update(context.Context, *takeoverdomain.TakeoverAc) error { return nil }
func (m *takeoverServiceMock) Delete(context.Context, []byte, []byte) error             { return nil }
func (m *takeoverServiceMock) AttachFile(context.Context, []byte, []byte) error         { return nil }
func (m *takeoverServiceMock) GetByID(ctx context.Context, id []byte) (*takeoverdomain.TakeoverAc, error) {
	if m.getByIDFn != nil {
		return m.getByIDFn(ctx, id)
	}
	return nil, nil
}
func (m *takeoverServiceMock) List(context.Context, int, int) ([]takeoverdomain.TakeoverAc, int64, error) {
	return nil, 0, nil
}

func newFlightApp(h *FlightHandler, withActor bool) *fiber.App {
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		if withActor {
			c.Locals("user_id", uuidv7.MustBytes())
		}
		return c.Next()
	})
	app.Post("/flights", h.Create)
	app.Get("/flights/me", h.GetByAuthUser)
	app.Get("/flights", h.List)
	return app
}

func doReqFlight(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, err := http.NewRequest(method, path, rdr)
	if err != nil {
		t.Fatalf("new req: %v", err)
	}
	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 TestFlightHandler(t *testing.T) {
	t.Run("new and with reserve ac", func(t *testing.T) {
		h := NewFlightHandler(&flightServiceMock{})
		if h == nil || h.validate == nil {
			t.Fatal("expected handler initialized")
		}
		if h.WithReserveAcService(nil) != h {
			t.Fatal("expected same pointer")
		}
	})

	t.Run("create invalid json", func(t *testing.T) {
		h := NewFlightHandler(&flightServiceMock{})
		code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", []byte(`{"data":`))
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}
	})

	t.Run("create validation", func(t *testing.T) {
		h := NewFlightHandler(&flightServiceMock{})
		code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", []byte(`{}`))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
	})

	t.Run("create date invalid", func(t *testing.T) {
		h := NewFlightHandler(&flightServiceMock{})
		body := []byte(`{"data":{"type":"flight","attributes":{"date":"x"}}}`)
		code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", body)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
	})

	t.Run("create optional uuid invalid", func(t *testing.T) {
		h := NewFlightHandler(&flightServiceMock{})
		body := []byte(`{"data":{"type":"flight","attributes":{"date":"2026-04-14","duty_roster_id":"bad"}}}`)
		code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", body)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
		body = []byte(`{"data":{"type":"flight","attributes":{"date":"2026-04-14","reserve_ac_id":"bad"}}}`)
		code, _ = doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", body)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 got %d", code)
		}
	})

	t.Run("create service error", func(t *testing.T) {
		h := NewFlightHandler(&flightServiceMock{
			createFn: func(context.Context, *flight.Flight) error { return errors.New("boom") },
		})
		body := []byte(`{"data":{"type":"flight","attributes":{"date":"2026-04-14"}}}`)
		code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", body)
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}
	})

	t.Run("create success with refetch and fallback", func(t *testing.T) {
		var createdID []byte
		h := NewFlightHandler(&flightServiceMock{
			createFn: func(_ context.Context, row *flight.Flight) error {
				row.ID = uuidv7.MustBytes()
				createdID = append([]byte(nil), row.ID...)
				return nil
			},
			getByIDFn: func(_ context.Context, id []byte) (*flight.Flight, error) {
				if string(id) != string(createdID) {
					t.Fatal("unexpected id")
				}
				return &flight.Flight{
					ID:        id,
					Date:      time.Date(2026, 4, 14, 0, 0, 0, 0, time.UTC),
					CreatedAt: time.Now().UTC(),
					UpdatedAt: time.Now().UTC(),
				}, nil
			},
		})
		body := []byte(`{"data":{"type":"flight","attributes":{"date":"2026-04-14","duty_roster_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","reserve_ac_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}}}`)
		code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", body)
		if code != http.StatusCreated {
			t.Fatalf("expected 201 got %d", code)
		}

		h2 := NewFlightHandler(&flightServiceMock{
			createFn: func(_ context.Context, row *flight.Flight) error {
				row.ID = uuidv7.MustBytes()
				row.CreatedAt = time.Now().UTC()
				row.UpdatedAt = row.CreatedAt
				return nil
			},
			getByIDFn: func(context.Context, []byte) (*flight.Flight, error) { return nil, errors.New("x") },
		})
		code, _ = doReqFlight(t, newFlightApp(h2, true), http.MethodPost, "/flights", []byte(`{"data":{"type":"flight","attributes":{"date":"2026-04-14"}}}`))
		if code != http.StatusCreated {
			t.Fatalf("expected 201 got %d", code)
		}
	})

	t.Run("get by auth user branches", func(t *testing.T) {
		h := NewFlightHandler(&flightServiceMock{})
		code, _ := doReqFlight(t, newFlightApp(h, false), http.MethodGet, "/flights/me", nil)
		if code != http.StatusUnauthorized {
			t.Fatalf("expected 401 got %d", code)
		}

		hErr := NewFlightHandler(&flightServiceMock{
			listByUserIDFn: func(context.Context, []byte, int, int) ([]flight.Flight, int64, error) {
				return nil, 0, errors.New("err")
			},
		})
		code, _ = doReqFlight(t, newFlightApp(hErr, true), http.MethodGet, "/flights/me?page[number]=0&page[size]=500", nil)
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}

		row := flight.Flight{
			ID:           uuidv7.MustBytes(),
			Date:         time.Date(2026, 4, 14, 0, 0, 0, 0, time.UTC),
			DutyRosterID: uuidv7.MustBytes(),
			ReserveAcID:  uuidv7.MustBytes(),
			CreatedAt:    time.Now().UTC(),
			UpdatedAt:    time.Now().UTC(),
		}

		hOK := NewFlightHandler(&flightServiceMock{
			listByUserIDFn: func(_ context.Context, _ []byte, limit, offset int) ([]flight.Flight, int64, error) {
				if limit != 100 || offset != 0 {
					t.Fatalf("unexpected paging %d %d", limit, offset)
				}
				return []flight.Flight{row}, 1, nil
			},
		})
		code, body := doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights/me?page[number]=0&page[size]=500", nil)
		if code != http.StatusOK || !strings.Contains(body, `"flight"`) {
			t.Fatalf("expected 200 flight body, got %d %s", code, body)
		}

		hPg := NewFlightHandler(&flightServiceMock{
			listByUserIDFn: func(_ context.Context, _ []byte, limit, offset int) ([]flight.Flight, int64, error) {
				if limit <= 0 || offset != 0 {
					t.Fatalf("unexpected paging")
				}
				return nil, 0, nil
			},
		})
		code, _ = doReqFlight(t, newFlightApp(hPg, true), http.MethodGet, "/flights/me?page=1&limit=5", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
		code, _ = doReqFlight(t, newFlightApp(hPg, true), http.MethodGet, "/flights/me?page=1&size=7", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
		code, _ = doReqFlight(t, newFlightApp(hPg, true), http.MethodGet, "/flights/me?page=-1&page[size]=-1", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
		code, _ = doReqFlight(t, newFlightApp(hPg, true), http.MethodGet, "/flights/me?page=1&page[size]=-1&limit=-1&size=-1", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
	})

	t.Run("list branches", func(t *testing.T) {
		hErr := NewFlightHandler(&flightServiceMock{
			listFn: func(context.Context, string, string, string, int, int) ([]flight.Flight, int64, error) {
				return nil, 0, errors.New("no")
			},
		})
		code, _ := doReqFlight(t, newFlightApp(hErr, true), http.MethodGet, "/flights?search=x", nil)
		if code != http.StatusBadRequest {
			t.Fatalf("expected 400 got %d", code)
		}

		hOK := NewFlightHandler(&flightServiceMock{
			listFn: func(_ context.Context, filter, date, sort string, limit, offset int) ([]flight.Flight, int64, error) {
				if filter != "abc" || sort != "-date" || limit != 0 || offset != 0 {
					t.Fatalf("unexpected args")
				}
				return []flight.Flight{{ID: uuidv7.MustBytes(), Date: time.Now().UTC(), CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}}, 1, nil
			},
		}).WithMissionService(&missionServiceMock{
			listByFlightIDFn: func(_ context.Context, _ []byte) ([]mission.Mission, error) {
				return []mission.Mission{{ID: uuidv7.MustBytes(), Type: "CAT", CreatedAt: time.Now().UTC()}}, nil
			},
		})
		code, body := doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?filter[search]=abc&sort=-date&page[number]=0&page[size]=0", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
		if !strings.Contains(body, `"mission_id"`) {
			t.Fatalf("expected mission_id in response, got %s", body)
		}

		hProgress := NewFlightHandler(&flightServiceMock{
			listFn: func(_ context.Context, filter, date, sort string, limit, offset int) ([]flight.Flight, int64, error) {
				if filter != "" || date != "" || sort != "" || limit != 0 || offset != 0 {
					t.Fatalf("unexpected args")
				}
				return []flight.Flight{{
					ID:           uuidv7.MustBytes(),
					Date:         time.Now().UTC(),
					TakeoverAcID: uuidv7.MustBytes(),
					Takeover: &takeoverdomain.TakeoverAc{
						ReserveAcID: uuidv7.MustBytes(),
						ReserveAc:   &reserveac.ReserveAc{Status: "onprogress"},
					},
					CreatedAt: time.Now().UTC(),
					UpdatedAt: time.Now().UTC(),
				}}, 1, nil
			},
		})
		code, body = doReqFlight(t, newFlightApp(hProgress, true), http.MethodGet, "/flights", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
		if !strings.Contains(body, `"status":"in_progress"`) {
			t.Fatalf("expected on_progress status, got %s", body)
		}
		if !strings.Contains(body, `"complete":false`) {
			t.Fatalf("expected complete=false in response, got %s", body)
		}

		inspectionID := uuidv7.MustBytes()
		afterID := uuidv7.MustBytes()
		hAfter := NewFlightHandler(&flightServiceMock{
			listFn: func(_ context.Context, filter, date, sort string, limit, offset int) ([]flight.Flight, int64, error) {
				if filter != "" || date != "" || sort != "" || limit != 0 || offset != 0 {
					t.Fatalf("unexpected args")
				}
				return []flight.Flight{{
					ID:           uuidv7.MustBytes(),
					Date:         time.Now().UTC(),
					TakeoverAcID: uuidv7.MustBytes(),
					Takeover: &takeoverdomain.TakeoverAc{
						ReserveAcID: uuidv7.MustBytes(),
						ReserveAc: &reserveac.ReserveAc{
							Inspection: &flightinspection.FlightInspection{ID: inspectionID},
						},
					},
					CreatedAt: time.Now().UTC(),
					UpdatedAt: time.Now().UTC(),
				}}, 1, nil
			},
		}).WithMissionService(&missionServiceMock{
			listByFlightIDFn: func(_ context.Context, _ []byte) ([]mission.Mission, error) {
				return []mission.Mission{{ID: uuidv7.MustBytes(), Type: "CAT", CreatedAt: time.Now().UTC()}}, nil
			},
		}).WithAfterFlightInspectionService(&afterFlightInspectionServiceMock{
			getByFlightInspectionIDFn: func(_ context.Context, id []byte) (*afterflightinspection.AfterFlightInspection, error) {
				if string(id) != string(inspectionID) {
					t.Fatalf("unexpected inspection id")
				}
				now := time.Now().UTC()
				return &afterflightinspection.AfterFlightInspection{
					ID:                 afterID,
					FlightInspectionID: inspectionID,
					CreatedAt:          now,
					CreatedBy:          uuidv7.MustBytes(),
					UpdatedAt:          now,
					UpdatedBy:          uuidv7.MustBytes(),
				}, nil
			},
		}).WithTakeoverService(&takeoverServiceMock{
			getByIDFn: func(_ context.Context, id []byte) (*takeoverdomain.TakeoverAc, error) {
				if len(id) != 16 {
					t.Fatalf("unexpected takeover id")
				}
				return &takeoverdomain.TakeoverAc{
					ID:     id,
					BaseID: uuidv7.MustBytes(),
					ReserveAc: &reserveac.ReserveAc{
						AircraftID: uuidv7.MustBytes(),
						Inspection: &flightinspection.FlightInspection{ID: inspectionID},
					},
					RosterCrews: []takeoverdomain.TakeoverRosterCrew{{ID: uuidv7.MustBytes()}},
					OtherPeople: []takeoverdomain.TakeoverOtherPerson{{ID: uuidv7.MustBytes()}},
				}, nil
			},
		})
		code, body = doReqFlight(t, newFlightApp(hAfter, true), http.MethodGet, "/flights", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
		if !strings.Contains(body, `"phase":"after_flight_inspection"`) {
			t.Fatalf("expected after_flight_inspection step, got %s", body)
		}
		if !strings.Contains(body, `"step":3`) {
			t.Fatalf("expected step 3 in response, got %s", body)
		}
		if !strings.Contains(body, `"complete":true`) {
			t.Fatalf("expected complete=true in response, got %s", body)
		}

		code, _ = doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?page=1&limit=5&search=abc&sort=-date", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
		code, _ = doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?page=1&size=7&search=abc&sort=-date", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
		code, _ = doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?page=-1&page[size]=-1&search=abc&sort=-date", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
		code, _ = doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?page=1&page[size]=-1&limit=-1&size=-1&search=abc&sort=-date", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
		code, _ = doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?page=1&page[size]=200&search=abc&sort=-date", nil)
		if code != http.StatusOK {
			t.Fatalf("expected 200 got %d", code)
		}
	})

	t.Run("helpers", func(t *testing.T) {
		if stringPtrOrEmpty(nil) != "" {
			t.Fatal("expected empty")
		}
		v := "  x "
		if stringPtrOrEmpty(&v) != "x" {
			t.Fatal("expected trimmed")
		}
		if timeStringOrEmptyRefTime(nil) != "" || timeStringOrEmptyRefTime(&time.Time{}) != "" {
			t.Fatal("expected empty")
		}
		now := time.Now().UTC()
		if timeStringOrEmptyRefTime(&now) == "" {
			t.Fatal("expected time")
		}
		raw := "bad"
		if _, errObj := parseOptionalUUIDFlight(&raw, "/data/attributes/duty_roster_id"); errObj == nil {
			t.Fatal("expected err")
		}
		raw = " "
		if id, errObj := parseOptionalUUIDFlight(&raw, "/data/attributes/duty_roster_id"); errObj != nil || id != nil {
			t.Fatal("expected nil")
		}
		raw = mustUUIDStringHospital(t, uuidv7.MustBytes())
		if id, errObj := parseOptionalUUIDFlight(&raw, "/data/attributes/duty_roster_id"); errObj != nil || len(id) != 16 {
			t.Fatal("expected id")
		}
		if s := flightMissionStep(nil); s.Status != "unassigned" {
			t.Fatal("expected unassigned")
		}
		if s := flightDutyRosterStep(nil); s.Status != "unassigned" {
			t.Fatal("expected unassigned")
		}
		if s := flightReserveAcStep(nil); s.Status != "unassigned" {
			t.Fatal("expected unassigned")
		}
		if s := flightReserveAcStep(&flight.Flight{Takeover: &takeoverdomain.TakeoverAc{ReserveAcID: uuidv7.MustBytes(), ReserveAc: &reserveac.ReserveAc{Status: "pending"}}}); s.Status != "pending" {
			t.Fatal("expected pending")
		}
		if s := flightReserveAcStep(&flight.Flight{Takeover: &takeoverdomain.TakeoverAc{ReserveAcID: uuidv7.MustBytes(), ReserveAc: &reserveac.ReserveAc{Status: "onprogress"}}}); s.Status != "in_progress" {
			t.Fatal("expected on progress")
		}
		if s := flightReserveAcStep(&flight.Flight{Takeover: &takeoverdomain.TakeoverAc{ReserveAcID: uuidv7.MustBytes(), ReserveAc: &reserveac.ReserveAc{Status: "approved", UpdatedAt: time.Now().UTC(), UpdatedBy: uuidv7.MustBytes()}}}); s.Status != "completed" {
			t.Fatal("expected completed")
		}
		if s := flightMissionStep(&flight.Flight{}); s.Status != "unassigned" {
			t.Fatal("expected mission unassigned")
		}
	})
}

func TestFlightHandlerList_MissionStepShowsInProgressUntilFlightDataComplete(t *testing.T) {
	flightID := uuidv7.MustBytes()
	missionID := uuidv7.MustBytes()

	h := NewFlightHandler(&flightServiceMock{
		listFn: func(context.Context, string, string, string, int, int) ([]flight.Flight, int64, error) {
			return []flight.Flight{{
				ID:        flightID,
				Date:      time.Date(2026, time.June, 26, 0, 0, 0, 0, time.UTC),
				CreatedAt: time.Date(2026, time.June, 24, 4, 31, 37, 0, time.UTC),
				CreatedBy: []byte("creator-id-12345"),
			}}, 1, nil
		},
	}).WithMissionService(&missionServiceMock{
		listByFlightIDFn: func(context.Context, []byte) ([]mission.Mission, error) {
			return []mission.Mission{{
				ID:        missionID,
				FlightID:  flightID,
				Type:      "CAT",
				CreatedAt: time.Date(2026, time.June, 24, 5, 23, 3, 0, time.UTC),
				UpdatedAt: time.Date(2026, time.June, 24, 5, 23, 3, 0, time.UTC),
			}}, nil
		},
	}).WithFlightDataService(&flightDataServiceMock{
		getByMisFn: func(context.Context, []byte) (*flightdata.FlightData, error) {
			return &flightdata.FlightData{Status: flightdata.StatusInProgress}, nil
		},
	})

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_id", uuidv7.MustBytes())
		return c.Next()
	})
	app.Get("/flights", h.List)

	code, body := doReqFlight(t, app, http.MethodGet, "/flights?date=2026-06-26&limit=2000", nil)
	if code != http.StatusOK {
		t.Fatalf("unexpected status: %d body=%s", code, body)
	}
	if !strings.Contains(body, `"phase":"mission"`) {
		t.Fatalf("expected mission step in body: %s", body)
	}
	if !strings.Contains(body, `"status":"in_progress"`) {
		t.Fatalf("expected mission step in_progress: %s", body)
	}
}

func TestFlightHandlerList_MissionStepShowsCompletedWhenFlightDataComplete(t *testing.T) {
	flightID := uuidv7.MustBytes()
	missionID := uuidv7.MustBytes()

	h := NewFlightHandler(&flightServiceMock{
		listFn: func(context.Context, string, string, string, int, int) ([]flight.Flight, int64, error) {
			return []flight.Flight{{
				ID:        flightID,
				Date:      time.Date(2026, time.June, 26, 0, 0, 0, 0, time.UTC),
				CreatedAt: time.Date(2026, time.June, 24, 4, 31, 37, 0, time.UTC),
				CreatedBy: []byte("creator-id-12345"),
			}}, 1, nil
		},
	}).WithMissionService(&missionServiceMock{
		listByFlightIDFn: func(context.Context, []byte) ([]mission.Mission, error) {
			return []mission.Mission{{
				ID:        missionID,
				FlightID:  flightID,
				Type:      "CAT",
				CreatedAt: time.Date(2026, time.June, 24, 5, 23, 3, 0, time.UTC),
				UpdatedAt: time.Date(2026, time.June, 24, 6, 41, 45, 0, time.UTC),
				UpdatedBy: []byte("test test"),
			}}, nil
		},
	}).WithFlightDataService(&flightDataServiceMock{
		getByMisFn: func(context.Context, []byte) (*flightdata.FlightData, error) {
			return &flightdata.FlightData{Status: flightdata.StatusCompleted}, nil
		},
	})

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_id", uuidv7.MustBytes())
		return c.Next()
	})
	app.Get("/flights", h.List)

	code, body := doReqFlight(t, app, http.MethodGet, "/flights?date=2026-06-26&limit=2000", nil)
	if code != http.StatusOK {
		t.Fatalf("unexpected status: %d body=%s", code, body)
	}
	if !strings.Contains(body, `"phase":"mission"`) {
		t.Fatalf("expected mission step in body: %s", body)
	}
	if !strings.Contains(body, `"status":"completed"`) {
		t.Fatalf("expected mission step completed: %s", body)
	}
}
