package handlers

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"

	actionsignoff "wucher/internal/domain/action_signoff"
	"wucher/internal/domain/complaint"
	"wucher/internal/shared/pkg/uuidv7"
)

type complaintSignoffMock struct {
	getByComplaintIDsFn func(context.Context, [][]byte) (map[string]*actionsignoff.ActionSignoff, error)
	unsignByComplaintFn func(context.Context, []byte) (*actionsignoff.ActionSignoff, error)
}

func (m *complaintSignoffMock) GetByComplaintIDs(ctx context.Context, ids [][]byte) (map[string]*actionsignoff.ActionSignoff, error) {
	if m.getByComplaintIDsFn != nil {
		return m.getByComplaintIDsFn(ctx, ids)
	}
	return map[string]*actionsignoff.ActionSignoff{}, nil
}

func (m *complaintSignoffMock) UnsignByComplaint(ctx context.Context, complaintID []byte) (*actionsignoff.ActionSignoff, error) {
	if m.unsignByComplaintFn != nil {
		return m.unsignByComplaintFn(ctx, complaintID)
	}
	return nil, nil
}

type complaintServiceMock struct {
	createFn           func(context.Context, *complaint.Complaint) error
	updateFn           func(context.Context, *complaint.Complaint) error
	getByIDFn          func(context.Context, []byte) (*complaint.Complaint, error)
	hasOpenFn          func(context.Context, []byte) (bool, error)
	listFn             func(context.Context, complaint.ListFilter, string, int, int) ([]complaint.Complaint, int64, error)
	listByHelicopterFn func(context.Context, []byte, int, int) ([]complaint.Complaint, int64, error)
	resolveNamesFn     func(context.Context, []complaint.Complaint) (map[string]string, error)
}

func (m *complaintServiceMock) Create(ctx context.Context, row *complaint.Complaint) error {
	if m.createFn != nil {
		return m.createFn(ctx, row)
	}
	return nil
}

func (m *complaintServiceMock) Update(ctx context.Context, row *complaint.Complaint) error {
	if m.updateFn != nil {
		return m.updateFn(ctx, row)
	}
	return nil
}

func (m *complaintServiceMock) Delete(context.Context, []byte, []byte) error { return nil }

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

func (m *complaintServiceMock) ListByHelicopter(ctx context.Context, helicopterID []byte, limit, offset int) ([]complaint.Complaint, int64, error) {
	if m.listByHelicopterFn != nil {
		return m.listByHelicopterFn(ctx, helicopterID, limit, offset)
	}
	return nil, 0, nil
}

func (m *complaintServiceMock) List(ctx context.Context, filter complaint.ListFilter, sort string, limit, offset int) ([]complaint.Complaint, int64, error) {
	if m.listFn != nil {
		return m.listFn(ctx, filter, sort, limit, offset)
	}
	return nil, 0, nil
}

func (m *complaintServiceMock) HelicopterHasOpenComplaint(ctx context.Context, helicopterID []byte) (bool, error) {
	if m.hasOpenFn != nil {
		return m.hasOpenFn(ctx, helicopterID)
	}
	return false, nil
}

func (m *complaintServiceMock) ActiveGroundingByHelicopterIDs(context.Context, [][]byte) ([]complaint.Complaint, error) {
	return nil, nil
}
func (m *complaintServiceMock) OpenByHelicopterIDs(context.Context, [][]byte) ([]complaint.Complaint, error) {
	return nil, nil
}

func (m *complaintServiceMock) HelicopterHasUnactionedComplaint(context.Context, []byte) (bool, error) {
	return false, nil
}

func (m *complaintServiceMock) MarkFixedByHelicopter(context.Context, []byte, []byte, []byte) error {
	return nil
}

func (m *complaintServiceMock) MarkFixedByComplaint(context.Context, []byte, []byte, []byte) error {
	return nil
}

func (m *complaintServiceMock) MarkFixedByFlight(context.Context, []byte, []byte, []byte) error {
	return nil
}

func (m *complaintServiceMock) ResolveUserNames(ctx context.Context, rows []complaint.Complaint) (map[string]string, error) {
	if m.resolveNamesFn != nil {
		return m.resolveNamesFn(ctx, rows)
	}
	return map[string]string{}, nil
}

func newComplaintApp(h *ComplaintHandler, userID []byte) *fiber.App {
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_id", userID)
		return c.Next()
	})
	app.Post("/api/v1/complaints/create", h.Create)
	app.Get("/api/v1/complaints/get/:id", h.GetByID)
	app.Patch("/api/v1/complaints/update/:id", h.Update)
	app.Post("/api/v1/complaints/action-taken/create/:id", h.ActionTaken)
	app.Patch("/api/v1/complaints/action-taken/update/:id", h.UpdateActionTaken)
	app.Post("/api/v1/complaints/nsr/sign/:id", h.NSRSign)
	app.Post("/api/v1/complaints/nsr/unsign/:id", h.NSRUnsign)
	app.Get("/api/v1/complaints/hold-items/get-all", h.ListHoldItems)
	app.Get("/api/v1/complaints/hold-items/get-all/dt", h.ListHoldItemsDatatable)
	return app
}

func TestComplaintHandler_ListHoldItems_FiltersOpenMEL(t *testing.T) {
	var got complaint.ListFilter
	svc := &complaintServiceMock{
		listFn: func(_ context.Context, filter complaint.ListFilter, _ string, _, _ int) ([]complaint.Complaint, int64, error) {
			got = filter
			return nil, 0, nil
		},
	}
	app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())

	req, _ := http.NewRequest(http.MethodGet, "/api/v1/complaints/hold-items/get-all?date_from=2026-06-01&date_to=2026-06-30&person=Ganahl&search=vibration", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	bodyBytes, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
	if !got.HoldItemsOnly {
		t.Fatalf("expected HoldItemsOnly=true in filter, got %#v", got)
	}
	if got.DateFrom != "2026-06-01" || got.DateTo != "2026-06-30" || got.Person != "Ganahl" || got.Search != "vibration" {
		t.Fatalf("expected Von/bis/person/search filters passed, got %#v", got)
	}
}

// Action-taken now only records the corrective action TEXT — signing moved to
// POST /action-signoffs/sign (with complaint_id). No sign field, no 422.
func TestComplaintHandler_ActionTaken_RecordsTextWithoutSign(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	helicopterID := uuidv7.MustBytes()

	var recorded *complaint.Complaint
	svc := &complaintServiceMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			if recorded != nil {
				return recorded, nil
			}
			return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: func() *time.Time { t := time.Now().UTC(); return &t }()}, nil
		},
		updateFn: func(_ context.Context, row *complaint.Complaint) error {
			if row.ActionTaken == nil || *row.ActionTaken != "tightened mount" {
				t.Fatalf("unexpected action_taken: %#v", row.ActionTaken)
			}
			recorded = row
			return nil
		},
	}
	app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())

	// No `sign` field anymore.
	body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":"tightened mount"}}}`
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/action-taken/create/"+complaintIDStr, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	bodyBytes, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
	if recorded == nil {
		t.Fatal("expected update to be called recording the action text")
	}
}

// A pending (unclassified) complaint — mel_classified_at NULL — cannot record action
// taken until MEL is classified; the gate returns 422 and never touches the row.
func TestComplaintHandler_ActionTaken_BlockedWhenMELPending(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	helicopterID := uuidv7.MustBytes()

	svc := &complaintServiceMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			// Pending: no MELClassifiedAt.
			return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration"}, nil
		},
		updateFn: func(context.Context, *complaint.Complaint) error {
			t.Fatal("update must not be called while MEL is pending")
			return nil
		},
	}
	app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())

	body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":"tightened mount"}}}`
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/action-taken/create/"+complaintIDStr, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode != http.StatusUnprocessableEntity {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 422 (classify MEL first), got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
}

// NSR is its own flow: nsr/sign sets the deferral on a classified complaint; a pending
// (unclassified) complaint cannot be deferred and returns 422.
func TestComplaintHandler_NSRSign_SetsDeferralWhenClassified(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	helicopterID := uuidv7.MustBytes()
	classified := time.Now().UTC()

	var saved *complaint.Complaint
	svc := &complaintServiceMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			if saved != nil {
				return saved, nil
			}
			return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: &classified}, nil
		},
		updateFn: func(_ context.Context, row *complaint.Complaint) error {
			if !row.IsNSR || row.NSRDecidedAt == nil {
				t.Fatalf("expected NSR set with decided_at, got %#v", row)
			}
			saved = row
			return nil
		},
	}
	app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())

	body := `{"data":{"type":"complaint_nsr_sign","attributes":{"nsr_reason":"deferred under MEL B"}}}`
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/nsr/sign/"+complaintIDStr, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(b))
	}
	if saved == nil || !saved.IsNSR {
		t.Fatal("expected NSR to be recorded")
	}
}

func TestComplaintHandler_NSRSign_BlockedWhenMELPending(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	helicopterID := uuidv7.MustBytes()

	svc := &complaintServiceMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			// Pending: no MELClassifiedAt.
			return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration"}, nil
		},
		updateFn: func(context.Context, *complaint.Complaint) error {
			t.Fatal("update must not be called: cannot defer a pending complaint")
			return nil
		},
	}
	app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())

	body := `{"data":{"type":"complaint_nsr_sign","attributes":{}}}`
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/nsr/sign/"+complaintIDStr, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode != http.StatusUnprocessableEntity {
		b, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 422 (classify MEL first), got %d body=%s", resp.StatusCode, string(b))
	}
}

func TestComplaintHandler_NSRUnsign_ClearsDeferral(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	helicopterID := uuidv7.MustBytes()
	classified := time.Now().UTC()

	var saved *complaint.Complaint
	svc := &complaintServiceMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			if saved != nil {
				return saved, nil
			}
			return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: &classified, IsNSR: true, NSRDecidedAt: &classified}, nil
		},
		updateFn: func(_ context.Context, row *complaint.Complaint) error {
			if row.IsNSR || row.NSRDecidedAt != nil {
				t.Fatalf("expected NSR cleared, got %#v", row)
			}
			saved = row
			return nil
		},
	}
	app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())

	req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/nsr/unsign/"+complaintIDStr, nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(b))
	}
	if saved == nil || saved.IsNSR {
		t.Fatal("expected NSR to be cleared")
	}
}

func TestComplaintHandler_UpdateActionTaken_RecordsTextWithoutSign(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	helicopterID := uuidv7.MustBytes()

	var recorded *complaint.Complaint
	svc := &complaintServiceMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			if recorded != nil {
				return recorded, nil
			}
			return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: func() *time.Time { t := time.Now().UTC(); return &t }()}, nil
		},
		updateFn: func(_ context.Context, row *complaint.Complaint) error {
			if row.ActionTaken == nil || *row.ActionTaken != "re-torqued mount" {
				t.Fatalf("unexpected action_taken: %#v", row.ActionTaken)
			}
			recorded = row
			return nil
		},
	}
	app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())

	body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":"re-torqued mount"}}}`
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/action-taken/update/"+complaintIDStr, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	bodyBytes, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
	if recorded == nil {
		t.Fatal("expected update to be called recording the action text")
	}
}

// The complaint response reads its action-sign state from action_signoff (by
// complaint_id), not from the (now-unset) complaint.ActionSignedAt/By columns.
func TestComplaintHandler_GetByID_ReadsSignFromSignoff(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	helicopterID := uuidv7.MustBytes()
	signer := uuidv7.MustBytes()
	signedAt := time.Now().UTC()

	action := "replaced part"
	svc := &complaintServiceMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, ActionTaken: &action}, nil
		},
	}
	var lookedUp [][]byte
	signoffSvc := &complaintSignoffMock{
		getByComplaintIDsFn: func(_ context.Context, ids [][]byte) (map[string]*actionsignoff.ActionSignoff, error) {
			lookedUp = ids
			return map[string]*actionsignoff.ActionSignoff{
				string(complaintID): {ComplaintID: complaintID, HelicopterID: helicopterID, SignedAt: &signedAt, SignedBy: signer},
			}, nil
		},
	}
	app := newComplaintApp(NewComplaintHandler(svc).WithActionSignoffService(signoffSvc), uuidv7.MustBytes())

	req, _ := http.NewRequest(http.MethodGet, "/api/v1/complaints/get/"+complaintIDStr, nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	bodyBytes, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
	if len(lookedUp) != 1 || string(lookedUp[0]) != string(complaintID) {
		t.Fatalf("expected signoff lookup by complaint id, got %#v", lookedUp)
	}
	attrs := struct {
		ActionSigned   bool    `json:"action_signed"`
		ActionSignedAt *string `json:"action_signed_at"`
	}{}
	var payload struct {
		Data struct {
			Data struct {
				Attributes json.RawMessage `json:"attributes"`
			} `json:"data"`
		} `json:"data"`
	}
	if err := json.Unmarshal(bodyBytes, &payload); err != nil {
		t.Fatalf("bad json: %v body=%s", err, string(bodyBytes))
	}
	if err := json.Unmarshal(payload.Data.Data.Attributes, &attrs); err != nil {
		t.Fatalf("bad attributes json: %v body=%s", err, string(bodyBytes))
	}
	if !attrs.ActionSigned {
		t.Fatalf("expected action_signed=true from signoff, got body=%s", string(bodyBytes))
	}
	if attrs.ActionSignedAt == nil {
		t.Fatalf("expected action_signed_at from signoff, got body=%s", string(bodyBytes))
	}
}

type complaintAOGRecalcMock struct {
	fn func(context.Context, []byte) error
}

func (m *complaintAOGRecalcMock) RecalculateAOGByHelicopter(ctx context.Context, helicopterID []byte) error {
	if m.fn != nil {
		return m.fn(ctx, helicopterID)
	}
	return nil
}

// Updating a complaint that was already closed reopens its lifecycle: the complaint is
// unfixed, its corrective-action sign-off is unsigned, and AOG is recomputed.
func TestComplaintHandler_Update_ReopensAndUnsignsWhenClosed(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	helicopterID := uuidv7.MustBytes()
	easaID := uuidv7.MustBytes()
	fixedAt := time.Now().UTC()

	var updatedRow *complaint.Complaint
	svc := &complaintServiceMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			// A fresh CLOSED complaint on each call (fixed by a past EASA release).
			return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, FixedAt: &fixedAt, FixedByEASAID: easaID}, nil
		},
		updateFn: func(_ context.Context, row *complaint.Complaint) error {
			updatedRow = row
			return nil
		},
	}
	var unsignedComplaint []byte
	signoffSvc := &complaintSignoffMock{
		unsignByComplaintFn: func(_ context.Context, cid []byte) (*actionsignoff.ActionSignoff, error) {
			unsignedComplaint = cid
			return nil, nil
		},
	}
	var recalced []byte
	aog := &complaintAOGRecalcMock{fn: func(_ context.Context, id []byte) error { recalced = id; return nil }}

	h := NewComplaintHandler(svc).WithActionSignoffService(signoffSvc).WithAOGRecalculator(aog)
	app := newComplaintApp(h, uuidv7.MustBytes())

	body := `{"data":{"type":"complaint_update","id":"` + complaintIDStr + `","attributes":{"description":"vibration + extra work"}}}`
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/update/"+complaintIDStr, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	bodyBytes, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
	if updatedRow == nil {
		t.Fatal("expected update to be called")
	}
	if updatedRow.FixedAt != nil || len(updatedRow.FixedByEASAID) != 0 {
		t.Fatalf("expected complaint reopened (fixed_at/by cleared), got at=%v by=%x", updatedRow.FixedAt, updatedRow.FixedByEASAID)
	}
	if string(unsignedComplaint) != string(complaintID) {
		t.Fatalf("expected sign-off unsigned for complaint, got %x", unsignedComplaint)
	}
	if string(recalced) != string(helicopterID) {
		t.Fatalf("expected AOG recomputed for helicopter, got %x", recalced)
	}
}

func TestComplaintHandler_UpdateActionTaken_ClearWhenNotSigned(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	action := "old action"
	now := time.Now().UTC()

	var cleared bool
	svc := &complaintServiceMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			return &complaint.Complaint{ID: complaintID, HelicopterID: uuidv7.MustBytes(), MELSeverity: 2, MELClassifiedAt: &now, ActionTaken: &action}, nil
		},
		updateFn: func(_ context.Context, row *complaint.Complaint) error {
			if row.ActionTaken != nil {
				t.Fatalf("expected action_taken cleared to nil, got %v", *row.ActionTaken)
			}
			cleared = true
			return nil
		},
	}
	// unsigned sign-off → clearing allowed
	app := newComplaintApp(NewComplaintHandler(svc).WithActionSignoffService(&complaintSignoffMock{}), uuidv7.MustBytes())

	body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":""}}}`
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/action-taken/update/"+complaintIDStr, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	bodyBytes, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
	if !cleared {
		t.Fatal("expected action_taken to be cleared")
	}
}

func TestComplaintHandler_UpdateActionTaken_ClearBlockedWhenSigned(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	action := "old action"
	now := time.Now().UTC()

	svc := &complaintServiceMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			return &complaint.Complaint{ID: complaintID, HelicopterID: uuidv7.MustBytes(), MELSeverity: 2, MELClassifiedAt: &now, ActionTaken: &action}, nil
		},
		updateFn: func(context.Context, *complaint.Complaint) error {
			t.Fatal("must not update when clearing a signed action")
			return nil
		},
	}
	signoffSvc := &complaintSignoffMock{
		getByComplaintIDsFn: func(_ context.Context, _ [][]byte) (map[string]*actionsignoff.ActionSignoff, error) {
			return map[string]*actionsignoff.ActionSignoff{
				string(complaintID): {ComplaintID: complaintID, SignedAt: &now},
			}, nil
		},
	}
	app := newComplaintApp(NewComplaintHandler(svc).WithActionSignoffService(signoffSvc), uuidv7.MustBytes())

	body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":""}}}`
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/action-taken/update/"+complaintIDStr, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	bodyBytes, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusConflict {
		t.Fatalf("expected 409, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
	if !strings.Contains(string(bodyBytes), "COMPLAINT_ACTION_SIGNED_LOCKED") {
		t.Fatalf("expected COMPLAINT_ACTION_SIGNED_LOCKED, got %s", string(bodyBytes))
	}
}

func TestComplaintHandler_ActionTaken_CreateStillRequiresText(t *testing.T) {
	complaintIDStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	app := newComplaintApp(NewComplaintHandler(&complaintServiceMock{}), uuidv7.MustBytes())

	body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":""}}}`
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/action-taken/create/"+complaintIDStr, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode != http.StatusUnprocessableEntity {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("create with empty action_taken must be 422, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
}
