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"
	"wucher/internal/shared/pkg/uuidv7"
)

type actionSignoffComplaintMock struct {
	getByIDFn func(context.Context, []byte) (*complaint.Complaint, error)
}

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

type actionSignoffServiceMock struct {
	getByHelicopterFn   func(context.Context, []byte) (*actionsignoff.ActionSignoff, error)
	getByComplaintFn    func(context.Context, []byte) (*actionsignoff.ActionSignoff, error)
	getByComplaintIDsFn func(context.Context, [][]byte) (map[string]*actionsignoff.ActionSignoff, error)
	signFn              func(context.Context, []byte, []byte) (*actionsignoff.ActionSignoff, error)
	signForComplaintFn  func(context.Context, []byte, []byte, []byte) (*actionsignoff.ActionSignoff, error)
	unsignFn            func(context.Context, []byte, []byte) (*actionsignoff.ActionSignoff, error)
	unsignByComplaintFn func(context.Context, []byte) (*actionsignoff.ActionSignoff, error)
}

func (m *actionSignoffServiceMock) GetByFlight(ctx context.Context, flightID []byte) (*actionsignoff.ActionSignoff, error) {
	if m.getByHelicopterFn != nil {
		return m.getByHelicopterFn(ctx, flightID)
	}
	return nil, nil
}

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

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

func (m *actionSignoffServiceMock) SignForComplaint(ctx context.Context, complaintID, _ []byte, helicopterID []byte, signer []byte) (*actionsignoff.ActionSignoff, error) {
	if m.signForComplaintFn != nil {
		return m.signForComplaintFn(ctx, complaintID, helicopterID, signer)
	}
	return nil, nil
}

func (m *actionSignoffServiceMock) Sign(ctx context.Context, _ []byte, helicopterID []byte, signer []byte) (*actionsignoff.ActionSignoff, error) {
	if m.signFn != nil {
		return m.signFn(ctx, helicopterID, signer)
	}
	return nil, nil
}

func (m *actionSignoffServiceMock) Unsign(ctx context.Context, helicopterID []byte, actor []byte) (*actionsignoff.ActionSignoff, error) {
	if m.unsignFn != nil {
		return m.unsignFn(ctx, helicopterID, actor)
	}
	return nil, nil
}

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

func newActionSignoffApp(h *ActionSignoffHandler) *fiber.App {
	app := fiber.New()
	app.Get("/api/v1/action-signoffs/get-by-flight/:flight_id", h.GetByFlight)
	app.Post("/api/v1/action-signoffs/sign/:helicopter_id", h.Sign)
	return app
}

func TestActionSignoffSign_ComplaintLinked_RequiresActionTaken(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	heliID := uuidv7.MustBytes()
	heliStr, _ := uuidv7.BytesToString(heliID)

	svc := &actionSignoffServiceMock{
		signForComplaintFn: func(context.Context, []byte, []byte, []byte) (*actionsignoff.ActionSignoff, error) {
			t.Fatal("must not sign a complaint whose action_taken is empty")
			return nil, nil
		},
	}
	comp := &actionSignoffComplaintMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			// Complaint exists but action_taken not recorded yet.
			return &complaint.Complaint{ID: complaintID, HelicopterID: heliID}, nil
		},
	}
	app := newActionSignoffApp(NewActionSignoffHandler(svc).WithComplaintService(comp))

	body := `{"data":{"attributes":{"complaint_id":"` + complaintIDStr + `"}}}`
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/action-signoffs/sign/"+heliStr, 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.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
	var payload map[string]any
	_ = json.Unmarshal(bodyBytes, &payload)
	errs, _ := payload["errors"].([]any)
	if len(errs) == 0 || errs[0].(map[string]any)["code"] != "ACTION_SIGNOFF_ACTION_REQUIRED" {
		t.Fatalf("expected ACTION_SIGNOFF_ACTION_REQUIRED, got %s", string(bodyBytes))
	}
}

func TestActionSignoffSign_ComplaintLinked_Signs(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	heliID := uuidv7.MustBytes()
	heliStr, _ := uuidv7.BytesToString(heliID)
	action := "tightened mount"

	var signedComplaint []byte
	svc := &actionSignoffServiceMock{
		signForComplaintFn: func(_ context.Context, cid, hid []byte, _ []byte) (*actionsignoff.ActionSignoff, error) {
			signedComplaint = cid
			if string(hid) != string(heliID) {
				t.Fatalf("unexpected helicopter id: %x", hid)
			}
			now := time.Now().UTC()
			return &actionsignoff.ActionSignoff{HelicopterID: hid, ComplaintID: cid, SignedAt: &now}, nil
		},
	}
	comp := &actionSignoffComplaintMock{
		getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
			return &complaint.Complaint{ID: complaintID, HelicopterID: heliID, ActionTaken: &action}, nil
		},
	}
	app := newActionSignoffApp(NewActionSignoffHandler(svc).WithComplaintService(comp))

	flightStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	body := `{"data":{"attributes":{"complaint_id":"` + complaintIDStr + `","flight_id":"` + flightStr + `"}}}`
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/action-signoffs/sign/"+heliStr, 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 {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
	}
	if string(signedComplaint) != string(complaintID) {
		t.Fatalf("expected SignForComplaint with complaint id, got %x", signedComplaint)
	}
}

func TestActionSignoffSign_Standalone_NoComplaint(t *testing.T) {
	heliID := uuidv7.MustBytes()
	heliStr, _ := uuidv7.BytesToString(heliID)

	var standaloneSigned bool
	svc := &actionSignoffServiceMock{
		signFn: func(_ context.Context, hid []byte, _ []byte) (*actionsignoff.ActionSignoff, error) {
			standaloneSigned = true
			now := time.Now().UTC()
			return &actionsignoff.ActionSignoff{HelicopterID: hid, SignedAt: &now}, nil
		},
		signForComplaintFn: func(context.Context, []byte, []byte, []byte) (*actionsignoff.ActionSignoff, error) {
			t.Fatal("standalone sign must not call SignForComplaint")
			return nil, nil
		},
	}
	app := newActionSignoffApp(NewActionSignoffHandler(svc))

	// No complaint_id → standalone fleet sign-off (scoped to a flight).
	flightStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	body := `{"data":{"attributes":{"flight_id":"` + flightStr + `"}}}`
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/action-signoffs/sign/"+heliStr, 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 || !standaloneSigned {
		t.Fatalf("expected standalone sign, status=%d signed=%v", resp.StatusCode, standaloneSigned)
	}
}

func TestActionSignoffSign_ComplaintLinked_Unsign(t *testing.T) {
	complaintID := uuidv7.MustBytes()
	complaintIDStr, _ := uuidv7.BytesToString(complaintID)
	heliID := uuidv7.MustBytes()
	heliStr, _ := uuidv7.BytesToString(heliID)

	var unsigned []byte
	svc := &actionSignoffServiceMock{
		unsignByComplaintFn: func(_ context.Context, cid []byte) (*actionsignoff.ActionSignoff, error) {
			unsigned = cid
			return &actionsignoff.ActionSignoff{HelicopterID: heliID, ComplaintID: cid}, nil // SignedAt nil → unsigned
		},
		signForComplaintFn: func(context.Context, []byte, []byte, []byte) (*actionsignoff.ActionSignoff, error) {
			t.Fatal("sign:false must not call SignForComplaint")
			return nil, nil
		},
	}
	app := newActionSignoffApp(NewActionSignoffHandler(svc))

	body := `{"data":{"attributes":{"sign":false,"complaint_id":"` + complaintIDStr + `"}}}`
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/action-signoffs/sign/"+heliStr, 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 string(unsigned) != string(complaintID) {
		t.Fatalf("expected UnsignByComplaint with complaint id, got %x", unsigned)
	}
}

func TestActionSignoffSign_Standalone_Unsign(t *testing.T) {
	heliID := uuidv7.MustBytes()
	heliStr, _ := uuidv7.BytesToString(heliID)
	flightID := uuidv7.MustBytes()
	flightStr, _ := uuidv7.BytesToString(flightID)

	var unsignedFlight []byte
	svc := &actionSignoffServiceMock{
		unsignFn: func(_ context.Context, fid []byte, _ []byte) (*actionsignoff.ActionSignoff, error) {
			unsignedFlight = fid
			return &actionsignoff.ActionSignoff{HelicopterID: heliID}, nil // unsigned
		},
		signFn: func(context.Context, []byte, []byte) (*actionsignoff.ActionSignoff, error) {
			t.Fatal("sign:false must not call Sign")
			return nil, nil
		},
	}
	app := newActionSignoffApp(NewActionSignoffHandler(svc))

	body := `{"data":{"attributes":{"sign":false,"flight_id":"` + flightStr + `"}}}`
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/action-signoffs/sign/"+heliStr, 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 || string(unsignedFlight) != string(flightID) {
		b, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected standalone unsign, status=%d body=%s", resp.StatusCode, string(b))
	}
}

// The response must be single-`data` enveloped (response.Write/jsonapi adds the
// envelope). Wrapping the resource in ActionSignoffResponse here would double-nest
// it as {"data":{"data":...}}, which the frontend mapper reads as unsigned —
// leaving the SIGN checkbox unchecked even after a successful sign.
func TestActionSignoffGetByHelicopterIsSingleDataEnvelope(t *testing.T) {
	signedAt := time.Date(2026, 6, 30, 13, 18, 58, 0, time.UTC)
	heliID := uuidv7.MustBytes()
	heliStr, _ := uuidv7.BytesToString(heliID)

	svc := &actionSignoffServiceMock{
		getByHelicopterFn: func(_ context.Context, _ []byte) (*actionsignoff.ActionSignoff, error) {
			return &actionsignoff.ActionSignoff{
				ID:           uuidv7.MustBytes(),
				HelicopterID: heliID,
				SignedAt:     &signedAt,
				SignedBy:     uuidv7.MustBytes(),
			}, nil
		},
	}
	app := newActionSignoffApp(NewActionSignoffHandler(svc))

	req, _ := http.NewRequest(http.MethodGet, "/api/v1/action-signoffs/get-by-flight/"+heliStr, 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"])
	}
	// Regression guard: there must be no nested data.data.
	if _, nested := data["data"]; nested {
		t.Fatalf("response is double-wrapped (data.data present): %#v", data)
	}
	attrs, ok := data["attributes"].(map[string]any)
	if !ok {
		t.Fatalf("expected data.attributes object, got %#v", data["attributes"])
	}
	if signed, _ := attrs["signed"].(bool); !signed {
		t.Fatalf("expected attributes.signed=true, got %#v", attrs["signed"])
	}
}

// Placeholder (no row yet) must also be single-`data` enveloped.
func TestActionSignoffGetByHelicopterPlaceholderIsSingleDataEnvelope(t *testing.T) {
	heliID := uuidv7.MustBytes()
	heliStr, _ := uuidv7.BytesToString(heliID)

	svc := &actionSignoffServiceMock{
		getByHelicopterFn: func(_ context.Context, _ []byte) (*actionsignoff.ActionSignoff, error) {
			return nil, nil
		},
	}
	app := newActionSignoffApp(NewActionSignoffHandler(svc))

	req, _ := http.NewRequest(http.MethodGet, "/api/v1/action-signoffs/get-by-flight/"+heliStr, nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}

	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("placeholder is double-wrapped (data.data present): %#v", data)
	}
	attrs, ok := data["attributes"].(map[string]any)
	if !ok {
		t.Fatalf("expected data.attributes object, got %#v", data["attributes"])
	}
	if signed, _ := attrs["signed"].(bool); signed {
		t.Fatalf("expected placeholder attributes.signed=false, got %#v", attrs["signed"])
	}
}
