package handlers

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

	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"

	complaint "wucher/internal/domain/complaint"
	easarelease "wucher/internal/domain/easa_release"
	fleethistory "wucher/internal/domain/fleet_history"
	fleetstatus "wucher/internal/domain/fleet_status"
	helicopterusage "wucher/internal/domain/helicopter_usage"
	"wucher/internal/domain/mcf"
	"wucher/internal/shared/pkg/uuidv7"
	responsedto "wucher/internal/transport/http/dto/response"
	shareddto "wucher/internal/transport/http/dto/shared"
)

func TestBuildFleetMCF(t *testing.T) {
	f64 := func(v float64) *float64 { return &v }
	summary := shareddto.FleetStatusSummary{
		Airframe: shareddto.FleetStatusSummaryAirframe{Hours: f64(1245.3)},
		Landings: f64(320),
	}

	t.Run("placeholder when no MCF is empty (no signed values)", func(t *testing.T) {
		got := buildFleetMCF(context.Background(), nil, summary)
		if got.ID != "" || got.Sign {
			t.Fatalf("placeholder must have empty id + unsigned, got %#v", got)
		}
		if got.AFHours != nil || got.LandingCount != nil {
			t.Fatalf("unsigned MCF must omit af_hours/landing_count, got %#v", got)
		}
	})

	t.Run("completed MCF stays with its snapshot + signer", func(t *testing.T) {
		completedAt := time.Now().UTC()
		row := &mcf.MaintenanceCheckFlight{ID: uuidv7.MustBytes(), AFHours: "1300", LandingCount: 5, CompletedAt: &completedAt, CompletedBy: uuidv7.MustBytes()}
		got := buildFleetMCF(context.Background(), row, summary)
		if got.ID == "" || !got.Sign {
			t.Fatalf("completed MCF must stay visible with id + sign=true, got %#v", got)
		}
		if got.AFHours == nil || *got.AFHours != "1300" || got.LandingCount == nil || *got.LandingCount != 5 {
			t.Fatalf("completed MCF should show its own snapshot, got %#v", got)
		}
		if got.SignedBy == "" {
			t.Fatalf("completed MCF should expose signed_by, got %#v", got)
		}
	})

	t.Run("draft MCF is unsigned and omits af_hours/landing", func(t *testing.T) {
		got := buildFleetMCF(context.Background(), &mcf.MaintenanceCheckFlight{ID: uuidv7.MustBytes(), AFHours: "9999", LandingCount: 7}, summary)
		if got.Sign {
			t.Fatal("draft (not completed) MCF must be unsigned")
		}
		if got.SignedBy != "" {
			t.Fatal("draft MCF must not have a signer")
		}
		// Unsigned → af_hours/landing are the signed values, so they are omitted here.
		if got.AFHours != nil || got.LandingCount != nil {
			t.Fatalf("unsigned draft must omit af_hours/landing_count, got %#v", got)
		}
	})

	t.Run("cancelled MCF stays visible as a cancelled record", func(t *testing.T) {
		cancelledAt := time.Now().UTC()
		d := time.Date(2026, 7, 7, 0, 0, 0, 0, time.UTC)
		row := &mcf.MaintenanceCheckFlight{ID: uuidv7.MustBytes(), Date: &d, AFHours: "1300", LandingCount: 5, CancelledAt: &cancelledAt}
		got := buildFleetMCF(context.Background(), row, summary)
		if got.ID == "" {
			t.Fatal("cancelled MCF must surface its id (visible in fleet, not a blank placeholder)")
		}
		if got.Active || got.Sign {
			t.Fatalf("cancelled MCF must be active=false and sign=false, got %#v", got)
		}
		if got.Date == nil || *got.Date != "2026-07-07" {
			t.Fatalf("cancelled MCF should surface its date, got %#v", got.Date)
		}
		// Unsigned (cancelled) → af_hours/landing omitted.
		if got.AFHours != nil || got.LandingCount != nil {
			t.Fatalf("cancelled MCF must omit af_hours/landing_count, got %#v", got)
		}
	})

	t.Run("active flag reflects MCF status", func(t *testing.T) {
		now := time.Now().UTC()
		passed, failed := mcf.ResultPassed, mcf.ResultFailed
		cases := []struct {
			name string
			row  *mcf.MaintenanceCheckFlight
			want bool
		}{
			{"no MCF", nil, false},
			{"draft", &mcf.MaintenanceCheckFlight{ID: uuidv7.MustBytes()}, true},
			{"signed passed (done)", &mcf.MaintenanceCheckFlight{ID: uuidv7.MustBytes(), CompletedAt: &now, Result: &passed}, false},
			{"signed failed (done — once signed, done)", &mcf.MaintenanceCheckFlight{ID: uuidv7.MustBytes(), CompletedAt: &now, Result: &failed}, false},
			{"cancelled", &mcf.MaintenanceCheckFlight{ID: uuidv7.MustBytes(), CancelledAt: &now}, false},
		}
		for _, tc := range cases {
			if got := buildFleetMCF(context.Background(), tc.row, summary); got.Active != tc.want {
				t.Fatalf("%s: active = %v, want %v", tc.name, got.Active, tc.want)
			}
		}
	})
}

func TestSortFleetStatusResourcesByStatus(t *testing.T) {
	mk := func(status string) responsedto.FleetStatusResource {
		return responsedto.FleetStatusResource{
			Attributes: responsedto.FleetStatusAttributes{
				Helicopter: &shareddto.FleetStatusHelicopter{Status: status},
			},
		}
	}
	// Scrambled input including an unknown status (sorts last).
	resources := []responsedto.FleetStatusResource{
		mk("aog"), mk("mcf"), mk(""), mk("available"), mk("booked"),
	}
	sortFleetStatusResourcesByStatus(resources)
	want := []string{"mcf", "aog", "booked", "available", ""}
	for i, w := range want {
		if got := resources[i].Attributes.Helicopter.Status; got != w {
			t.Fatalf("index %d: want %q, got %q", i, w, got)
		}
	}
}

type fsStubComplaints struct{}

func (fsStubComplaints) ListByHelicopter(context.Context, []byte, int, int) ([]complaint.Complaint, int64, error) {
	return nil, 0, nil
}
func (fsStubComplaints) ResolveUserNames(context.Context, []complaint.Complaint) (map[string]string, error) {
	return nil, nil
}
func (fsStubComplaints) ActiveGroundingByHelicopterIDs(context.Context, [][]byte) ([]complaint.Complaint, error) {
	return nil, nil
}
func (fsStubComplaints) OpenByHelicopterIDs(context.Context, [][]byte) ([]complaint.Complaint, error) {
	return nil, nil
}

type fsStubEASA struct{}

func (fsStubEASA) GetLatestByHelicopter(context.Context, []byte) (*easarelease.EASARelease, error) {
	return nil, nil
}

type fsStubHistory struct{}

func (fsStubHistory) ListByHelicopter(context.Context, []byte, int, int) ([]fleethistory.FleetHistory, int64, error) {
	return nil, 0, nil
}

func fleetStatusTestDeps() FleetStatusDeps {
	return FleetStatusDeps{
		Helicopter: &helicopterServiceMock{},
		Complaint:  fsStubComplaints{},
		EASA:       fsStubEASA{},
		History:    fsStubHistory{},
		MCF:        &stubMCFSvc{},
		Storage:    &helicopterPresignStorageMock{},
	}
}

type fleetStatusServiceMock struct {
	createErr       error
	updateErr       error
	markServicedErr error
	getByID         *fleetstatus.FleetStatus
}

func (m *fleetStatusServiceMock) Create(context.Context, *fleetstatus.FleetStatus) error {
	return m.createErr
}

func (m *fleetStatusServiceMock) Update(context.Context, *fleetstatus.FleetStatus) error {
	return m.updateErr
}

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

func (m *fleetStatusServiceMock) GetByID(context.Context, []byte) (*fleetstatus.FleetStatus, error) {
	if m.getByID != nil {
		row := *m.getByID
		return &row, nil
	}
	return nil, nil
}

func (m *fleetStatusServiceMock) List(context.Context, string, string, int, int) ([]fleetstatus.FleetStatus, int64, error) {
	return nil, 0, nil
}

func (m *fleetStatusServiceMock) LatestByHelicopterIDs(context.Context, [][]byte) (map[string]*fleetstatus.FleetStatus, error) {
	return nil, nil
}

func (m *fleetStatusServiceMock) MarkServiced(context.Context, []byte, time.Time, []byte) error {
	return m.markServicedErr
}

func (m *fleetStatusServiceMock) ListServiceHistoryByHelicopterID(context.Context, []byte, int, int) ([]fleetstatus.FleetStatusServiceLog, int64, error) {
	return nil, 0, nil
}

func (m *fleetStatusServiceMock) GetSummaryByHelicopterID(context.Context, []byte) (*shareddto.FleetStatusSummary, error) {
	return nil, nil
}

func (m *fleetStatusServiceMock) ReviseSummary(context.Context, []byte, helicopterusage.ManualSummaryInput, []byte) error {
	return nil
}

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

func (m *fleetStatusServiceMock) OverdueInspections(context.Context, []byte) ([]fleetstatus.OverdueInspection, error) {
	return nil, nil
}

func TestFleetStatusHandler_CreateAndUpdateHideRawErrors(t *testing.T) {
	t.Run("create", func(t *testing.T) {
		svc := &fleetStatusServiceMock{createErr: errors.New("insert into fleet_status (helicopter_id) values (...)")}
		h := NewFleetStatusHandler(svc, fleetStatusTestDeps())
		app := fiber.New()
		app.Post("/api/v1/fleet-status/create", h.Create)

		heliID := uuidv7.MustBytes()
		heliStr, _ := uuidv7.BytesToString(heliID)
		payload := []byte(`{"data":{"type":"fleet_status_create","attributes":{"helicopter_id":"` + heliStr + `"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/fleet-status/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")

		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		body, _ := io.ReadAll(resp.Body)
		if resp.StatusCode != http.StatusInternalServerError {
			t.Fatalf("expected 500, got %d", resp.StatusCode)
		}
		if strings.Contains(string(body), "insert into fleet_status") {
			t.Fatalf("expected raw error to be hidden, got %s", string(body))
		}
		if !strings.Contains(string(body), "an internal error occurred") {
			t.Fatalf("expected generic internal message, got %s", string(body))
		}
	})

	t.Run("update", func(t *testing.T) {
		id := uuidv7.MustBytes()
		heliID := uuidv7.MustBytes()
		svc := &fleetStatusServiceMock{
			updateErr: errors.New("update fleet_status set helicopter_id = ..."),
			getByID: &fleetstatus.FleetStatus{
				ID:           id,
				HelicopterID: heliID,
			},
		}
		h := NewFleetStatusHandler(svc, fleetStatusTestDeps())
		app := fiber.New()
		app.Patch("/api/v1/fleet-status/update/:id", h.Update)

		idStr, _ := uuidv7.BytesToString(id)
		heliStr, _ := uuidv7.BytesToString(heliID)
		payload := []byte(`{"data":{"type":"fleet_status_update","id":"` + idStr + `","attributes":{"helicopter_id":"` + heliStr + `"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/fleet-status/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")

		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		body, _ := io.ReadAll(resp.Body)
		if resp.StatusCode != http.StatusInternalServerError {
			t.Fatalf("expected 500, got %d", resp.StatusCode)
		}
		if strings.Contains(string(body), "update fleet_status set") {
			t.Fatalf("expected raw error to be hidden, got %s", string(body))
		}
		if !strings.Contains(string(body), "an internal error occurred") {
			t.Fatalf("expected generic internal message, got %s", string(body))
		}
	})
}

func TestFleetStatusHandler_MarkServicedReturns404ForRecordNotFound(t *testing.T) {
	svc := &fleetStatusServiceMock{
		markServicedErr: errors.Join(errors.New("wrapped service error"), gorm.ErrRecordNotFound),
	}
	h := NewFleetStatusHandler(svc, fleetStatusTestDeps())
	app := fiber.New()
	app.Post("/api/v1/fleet-status/mark-serviced/:id", h.MarkServiced)

	id := uuidv7.MustBytes()
	idStr, _ := uuidv7.BytesToString(id)
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/fleet-status/mark-serviced/"+idStr, nil)

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusNotFound {
		t.Fatalf("expected 404, got %d", resp.StatusCode)
	}
	if !strings.Contains(string(body), `"detail":"not found"`) {
		t.Fatalf("expected generic not found detail, got %s", string(body))
	}
}
