package handlers

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

	"github.com/gofiber/fiber/v2"

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

type easaComplaintCheckerMock struct {
	activeGroundingFn func(context.Context, [][]byte) ([]complaint.Complaint, error)
}

func (m *easaComplaintCheckerMock) HelicopterHasUnactionedComplaint(context.Context, []byte) (bool, error) {
	return false, nil
}
func (m *easaComplaintCheckerMock) ActiveGroundingByHelicopterIDs(ctx context.Context, ids [][]byte) ([]complaint.Complaint, error) {
	if m.activeGroundingFn != nil {
		return m.activeGroundingFn(ctx, ids)
	}
	return nil, nil
}
func (m *easaComplaintCheckerMock) MarkFixedByHelicopter(context.Context, []byte, []byte, []byte) error {
	return nil
}
func (m *easaComplaintCheckerMock) MarkFixedByComplaint(context.Context, []byte, []byte, []byte) error {
	return nil
}
func (m *easaComplaintCheckerMock) MarkFixedByFlight(context.Context, []byte, []byte, []byte) error {
	return nil
}

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

func (m *easaSignoffCheckerMock) GetByFlight(ctx context.Context, id []byte) (*actionsignoff.ActionSignoff, error) {
	if m.getByHelicopterFn != nil {
		return m.getByHelicopterFn(ctx, id)
	}
	return nil, nil
}
func (m *easaSignoffCheckerMock) 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 *easaSignoffCheckerMock) Unsign(ctx context.Context, helicopterID []byte, actor []byte) (*actionsignoff.ActionSignoff, error) {
	if m.unsignFn != nil {
		return m.unsignFn(ctx, helicopterID, actor)
	}
	return nil, nil
}

type easaServiceMock struct {
	getByIDFn func(context.Context, []byte) (*easarelease.EASARelease, error)
	signFn    func(context.Context, []byte, []byte) error
}

func (m *easaServiceMock) Create(context.Context, *easarelease.EASARelease) error { return nil }
func (m *easaServiceMock) Update(context.Context, *easarelease.EASARelease) error { return nil }
func (m *easaServiceMock) Delete(context.Context, []byte, []byte) error           { return nil }
func (m *easaServiceMock) GetByID(ctx context.Context, id []byte) (*easarelease.EASARelease, error) {
	if m.getByIDFn != nil {
		return m.getByIDFn(ctx, id)
	}
	return nil, nil
}
func (m *easaServiceMock) GetByFlightID(_ context.Context, _ []byte) (*easarelease.EASARelease, error) {
	return nil, nil
}
func (m *easaServiceMock) GetByComplaintID(_ context.Context, _ []byte) (*easarelease.EASARelease, error) {
	return nil, nil
}
func (m *easaServiceMock) ListByHelicopter(context.Context, []byte) ([]easarelease.EASARelease, error) {
	return nil, nil
}
func (m *easaServiceMock) GetActiveByHelicopter(context.Context, []byte) (*easarelease.EASARelease, error) {
	return nil, nil
}
func (m *easaServiceMock) Sign(ctx context.Context, id []byte, actor []byte) error {
	if m.signFn != nil {
		return m.signFn(ctx, id, actor)
	}
	return nil
}
func (m *easaServiceMock) Amend(context.Context, []byte, string, []byte) ([]byte, error) {
	return nil, nil
}

// Single-resource responses must be a single `data` envelope carrying the
// resource (with its id). A double `data.data` wrap makes the frontend parse the
// id off the wrong level and send POST /easa-releases/sign/ with an empty id →
// 404. This guards the Create/GetByID/Update/Sign shape.
func TestEASAReleaseSingleResponseIsSingleDataEnvelopeWithID(t *testing.T) {
	id := uuidv7.MustBytes()
	idStr, _ := uuidv7.BytesToString(id)

	svc := &easaServiceMock{
		getByIDFn: func(_ context.Context, _ []byte) (*easarelease.EASARelease, error) {
			return &easarelease.EASARelease{ID: id, HelicopterID: uuidv7.MustBytes()}, nil
		},
	}
	app := fiber.New()
	app.Get("/api/v1/easa-releases/get/:id", NewEASAReleaseHandler(svc).GetByID)

	req, _ := http.NewRequest(http.MethodGet, "/api/v1/easa-releases/get/"+idStr, nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(body))
	}

	var body map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		t.Fatal(err)
	}
	data, ok := body["data"].(map[string]any)
	if !ok {
		t.Fatalf("expected object at data, got %#v", body["data"])
	}
	if _, nested := data["data"]; nested {
		t.Fatalf("response is double-wrapped (data.data present): %#v", data)
	}
	if gotID, _ := data["id"].(string); gotID != idStr {
		t.Fatalf("expected data.id=%q, got %#v", idStr, data["id"])
	}
}

// The release is gated on the corrective-action sign-off: an open complaint must be
// signed off (via complaint_id); with no open complaint a standalone sign-off must exist.
func TestEASAReleaseRequireActionSignoff(t *testing.T) {
	helicopterID := uuidv7.MustBytes()
	flightID := uuidv7.MustBytes()
	complaintID := uuidv7.MustBytes()
	signedAt := time.Now().UTC()

	newHandler := func(complaintMock *easaComplaintCheckerMock, signoffMock *easaSignoffCheckerMock) *EASAReleaseHandler {
		return NewEASAReleaseHandler(&easaServiceMock{}).
			WithComplaintActionChecker(complaintMock).
			WithActionSignoffChecker(signoffMock)
	}

	t.Run("open complaint without sign-off is blocked", func(t *testing.T) {
		h := newHandler(
			&easaComplaintCheckerMock{activeGroundingFn: func(context.Context, [][]byte) ([]complaint.Complaint, error) {
				return []complaint.Complaint{{ID: complaintID, HelicopterID: helicopterID}}, nil
			}},
			&easaSignoffCheckerMock{}, // no sign-off for the complaint
		)
		aerr := h.requireActionSignoff(context.Background(), flightID, helicopterID)
		if aerr == nil || aerr.Code != apperrorsx.CodeEASAReleaseSignoffRequired {
			t.Fatalf("expected SIGNOFF_REQUIRED, got %#v", aerr)
		}
	})

	t.Run("open complaint with signed sign-off passes", func(t *testing.T) {
		h := newHandler(
			&easaComplaintCheckerMock{activeGroundingFn: func(context.Context, [][]byte) ([]complaint.Complaint, error) {
				return []complaint.Complaint{{ID: complaintID, HelicopterID: helicopterID}}, nil
			}},
			&easaSignoffCheckerMock{getByComplaintIDsFn: func(context.Context, [][]byte) (map[string]*actionsignoff.ActionSignoff, error) {
				return map[string]*actionsignoff.ActionSignoff{
					string(complaintID): {ComplaintID: complaintID, HelicopterID: helicopterID, SignedAt: &signedAt},
				}, nil
			}},
		)
		if aerr := h.requireActionSignoff(context.Background(), flightID, helicopterID); aerr != nil {
			t.Fatalf("expected pass, got %#v", aerr)
		}
	})

	t.Run("no complaint without standalone sign-off is blocked", func(t *testing.T) {
		h := newHandler(&easaComplaintCheckerMock{}, &easaSignoffCheckerMock{})
		aerr := h.requireActionSignoff(context.Background(), flightID, helicopterID)
		if aerr == nil || aerr.Code != apperrorsx.CodeEASAReleaseSignoffRequired {
			t.Fatalf("expected SIGNOFF_REQUIRED, got %#v", aerr)
		}
	})

	t.Run("no complaint with standalone signed sign-off passes", func(t *testing.T) {
		h := newHandler(&easaComplaintCheckerMock{}, &easaSignoffCheckerMock{
			getByHelicopterFn: func(context.Context, []byte) (*actionsignoff.ActionSignoff, error) {
				return &actionsignoff.ActionSignoff{HelicopterID: helicopterID, SignedAt: &signedAt}, nil
			},
		})
		if aerr := h.requireActionSignoff(context.Background(), flightID, helicopterID); aerr != nil {
			t.Fatalf("expected pass, got %#v", aerr)
		}
	})

	t.Run("gate is skipped when checkers are not wired", func(t *testing.T) {
		h := NewEASAReleaseHandler(&easaServiceMock{})
		if aerr := h.requireActionSignoff(context.Background(), flightID, helicopterID); aerr != nil {
			t.Fatalf("expected nil when unwired, got %#v", aerr)
		}
	})
}

// The gate is re-checked at sign time too: a draft whose sign-off was invalidated (e.g.
// its complaint was reopened) cannot be signed without a fresh sign-off.
func TestEASAReleaseSign_ReChecksSignoffGate(t *testing.T) {
	releaseID := uuidv7.MustBytes()
	releaseIDStr, _ := uuidv7.BytesToString(releaseID)
	helicopterID := uuidv7.MustBytes()
	complaintID := uuidv7.MustBytes()

	var signCalled bool
	svc := &easaServiceMock{
		getByIDFn: func(_ context.Context, _ []byte) (*easarelease.EASARelease, error) {
			return &easarelease.EASARelease{ID: releaseID, HelicopterID: helicopterID}, nil
		},
		signFn: func(context.Context, []byte, []byte) error { signCalled = true; return nil },
	}
	// Open complaint whose sign-off is NOT signed → gate must block.
	complaintMock := &easaComplaintCheckerMock{activeGroundingFn: func(context.Context, [][]byte) ([]complaint.Complaint, error) {
		return []complaint.Complaint{{ID: complaintID, HelicopterID: helicopterID}}, nil
	}}
	signoffMock := &easaSignoffCheckerMock{} // GetByComplaintIDs → empty → unsigned

	h := NewEASAReleaseHandler(svc).
		WithComplaintActionChecker(complaintMock).
		WithActionSignoffChecker(signoffMock)
	app := fiber.New()
	app.Post("/api/v1/easa-releases/sign/:id", h.Sign)

	req, _ := http.NewRequest(http.MethodPost, "/api/v1/easa-releases/sign/"+releaseIDStr, nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	bodyBytes, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
	if signCalled {
		t.Fatal("svc.Sign must not be called when the sign-off gate blocks")
	}
	if !strings.Contains(string(bodyBytes), apperrorsx.CodeEASAReleaseSignoffRequired) {
		t.Fatalf("expected SIGNOFF_REQUIRED, got %s", string(bodyBytes))
	}
}

// Signing a release consumes the fleet (standalone) sign-off so the next
// complaint-independent release requires a fresh signature (fixes staleness).
func TestEASAReleaseSign_ConsumesStandaloneSignoff(t *testing.T) {
	releaseID := uuidv7.MustBytes()
	releaseIDStr, _ := uuidv7.BytesToString(releaseID)
	helicopterID := uuidv7.MustBytes()
	flightID := uuidv7.MustBytes()
	signedAt := time.Now().UTC()

	svc := &easaServiceMock{
		getByIDFn: func(_ context.Context, _ []byte) (*easarelease.EASARelease, error) {
			return &easarelease.EASARelease{ID: releaseID, HelicopterID: helicopterID, FlightID: flightID}, nil
		},
	}
	// No open complaint → gate uses the standalone sign-off, which is signed → passes.
	complaintMock := &easaComplaintCheckerMock{}
	var consumed []byte
	signoffMock := &easaSignoffCheckerMock{
		getByHelicopterFn: func(context.Context, []byte) (*actionsignoff.ActionSignoff, error) {
			return &actionsignoff.ActionSignoff{HelicopterID: helicopterID, SignedAt: &signedAt}, nil
		},
		unsignFn: func(_ context.Context, hid []byte, _ []byte) (*actionsignoff.ActionSignoff, error) {
			consumed = hid
			return &actionsignoff.ActionSignoff{HelicopterID: hid}, nil
		},
	}

	h := NewEASAReleaseHandler(svc).
		WithComplaintActionChecker(complaintMock).
		WithActionSignoffChecker(signoffMock)
	app := fiber.New()
	app.Post("/api/v1/easa-releases/sign/:id", h.Sign)

	req, _ := http.NewRequest(http.MethodPost, "/api/v1/easa-releases/sign/"+releaseIDStr, nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
	if string(consumed) != string(flightID) {
		t.Fatalf("expected standalone sign-off consumed for flight, got %x", consumed)
	}
}
