package service

import (
	"context"
	"errors"
	"testing"
	"time"

	afterflightinspection "wucher/internal/domain/after_flight_inspection"
	flightdomain "wucher/internal/domain/flight"
	flightdata "wucher/internal/domain/flight_data"
	fmreport "wucher/internal/domain/fm_report"
	missiondomain "wucher/internal/domain/mission"
	reserveac "wucher/internal/domain/reserve_ac"
	takeoverdomain "wucher/internal/domain/takeover"
	sharedconst "wucher/internal/shared/const"
	"wucher/internal/shared/pkg/uuidv7"
)

type flightDataRepoStub struct {
	createFn                func(context.Context, *flightdata.FlightData) error
	updateFn                func(context.Context, *flightdata.FlightData) error
	getByIDFn               func(context.Context, []byte) (*flightdata.FlightData, error)
	getByMissionIDFn        func(context.Context, []byte) (*flightdata.FlightData, error)
	getSPODetailsByFlightID func(context.Context, []byte) (*flightdata.SPODetails, error)
}

type missionResolverStub struct {
	flightID []byte
	err      error
}

func (m missionResolverStub) FlightIDByMissionID(context.Context, []byte) ([]byte, error) {
	return append([]byte(nil), m.flightID...), m.err
}

type fmReportLookupStub struct {
	report *fmreport.Report
	err    error
}

type flightLookupStub struct {
	flight *flightdomain.Flight
	err    error
}

type afterFlightInspectionLookupStub struct {
	row *afterflightinspection.AfterFlightInspection
	err error
}

func (s fmReportLookupStub) GetByFlightID(context.Context, []byte) (*fmreport.Report, error) {
	return s.report, s.err
}

func (s flightLookupStub) GetByID(context.Context, []byte) (*flightdomain.Flight, error) {
	return s.flight, s.err
}

func (s afterFlightInspectionLookupStub) GetByFlightInspectionID(context.Context, []byte) (*afterflightinspection.AfterFlightInspection, error) {
	return s.row, s.err
}

func (r *flightDataRepoStub) Create(ctx context.Context, row *flightdata.FlightData) error {
	if r.createFn != nil {
		return r.createFn(ctx, row)
	}
	return nil
}

func (r *flightDataRepoStub) Update(ctx context.Context, row *flightdata.FlightData) error {
	if r.updateFn != nil {
		return r.updateFn(ctx, row)
	}
	return nil
}

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

func (r *flightDataRepoStub) GetByID(ctx context.Context, id []byte) (*flightdata.FlightData, error) {
	if r.getByIDFn != nil {
		return r.getByIDFn(ctx, id)
	}
	return nil, nil
}

func (r *flightDataRepoStub) GetByFlightID(context.Context, []byte) (*flightdata.FlightData, error) {
	return nil, nil
}

func (r *flightDataRepoStub) GetByMissionID(ctx context.Context, id []byte) (*flightdata.FlightData, error) {
	if r.getByMissionIDFn != nil {
		return r.getByMissionIDFn(ctx, id)
	}
	return nil, nil
}

func (r *flightDataRepoStub) ListByMissionID(context.Context, []byte) ([]flightdata.FlightData, error) {
	return nil, nil
}

func (r *flightDataRepoStub) List(context.Context, string, []byte, []byte, int, int) ([]flightdata.FlightData, int64, error) {
	return nil, 0, nil
}

func (r *flightDataRepoStub) GetSPODetailsByFlightDataID(ctx context.Context, id []byte) (*flightdata.SPODetails, error) {
	if r.getSPODetailsByFlightID != nil {
		return r.getSPODetailsByFlightID(ctx, id)
	}
	return nil, nil
}

func (r *flightDataRepoStub) UpsertSPODetails(context.Context, []byte, *flightdata.SPOUpsertData) error {
	return nil
}

func TestFlightDataServiceCreateSyncsCompleteStatus(t *testing.T) {
	var updates []*flightdata.FlightData
	rowID := []byte("019ecc1111111111")
	repo := &flightDataRepoStub{
		getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
			loaded := &flightdata.FlightData{
				ID:              id,
				MissionID:       []byte("1234567890123456"),
				CoPilotID:       []byte("2345678901234567"),
				FromICAOID:      []byte("3456789012345678"),
				ToHospitalID:    []byte("4567890123456789"),
				Mission:         &missiondomain.Mission{Type: sharedconst.MissionTypeCAT},
				FlightTakeOff:   time.Now().UTC(),
				FlightLanding:   time.Now().UTC(),
				FlightDuration:  time.Hour,
				LandingCount:    1,
				RotorBrakeCycle: 1,
				Status:          flightdata.StatusInProgress,
			}
			if got := flightdata.DeriveStatus(loaded, nil); got != flightdata.StatusCompleted {
				t.Fatalf("expected derived status complete, got %q", got)
			}
			return loaded, nil
		},
		updateFn: func(_ context.Context, row *flightdata.FlightData) error {
			updates = append(updates, row)
			return nil
		},
	}

	svc := NewFlightDataService(repo)
	if err := svc.syncStatus(context.Background(), rowID); err != nil {
		t.Fatalf("syncStatus returned error: %v", err)
	}
	if len(updates) != 1 {
		t.Fatalf("expected one status sync update, got %d", len(updates))
	}
	if updates[0].Status != flightdata.StatusCompleted {
		t.Fatalf("expected sync update to complete status, got %q", updates[0].Status)
	}
}

func TestFlightDataServiceCreateKeepsSPOIncompleteStatus(t *testing.T) {
	rowID := []byte("019ecc1122222222")
	repo := &flightDataRepoStub{
		getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
			loaded := &flightdata.FlightData{
				ID:              id,
				MissionID:       []byte("1234567890123456"),
				CoPilotID:       []byte("2345678901234567"),
				FromICAOID:      []byte("3456789012345678"),
				ToHospitalID:    []byte("4567890123456789"),
				FlightTakeOff:   time.Now().UTC(),
				FlightLanding:   time.Now().UTC(),
				FlightDuration:  time.Hour,
				LandingCount:    1,
				RotorBrakeCycle: 1,
				Mission:         &missiondomain.Mission{Type: sharedconst.MissionTypeSPO},
				Status:          flightdata.StatusInProgress,
			}
			if got := flightdata.DeriveStatus(loaded, nil); got != flightdata.StatusInProgress {
				t.Fatalf("expected derived status on_progress, got %q", got)
			}
			return loaded, nil
		},
		getSPODetailsByFlightID: func(context.Context, []byte) (*flightdata.SPODetails, error) {
			return &flightdata.SPODetails{}, nil
		},
	}

	svc := NewFlightDataService(repo)
	if err := svc.syncStatus(context.Background(), rowID); err != nil {
		t.Fatalf("syncStatus returned error: %v", err)
	}
}

func TestFlightDataServiceCreateSyncsCompleteStatusWithoutRotorBrakeCycleForCATAndNCO(t *testing.T) {
	for _, tc := range []struct {
		name string
		typ  string
	}{
		{name: "cat", typ: sharedconst.MissionTypeCAT},
		{name: "nco", typ: sharedconst.MissionTypeNCO},
	} {
		t.Run(tc.name, func(t *testing.T) {
			var updates []*flightdata.FlightData
			rowID := []byte("019ecc1144444444")
			repo := &flightDataRepoStub{
				getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
					loaded := &flightdata.FlightData{
						ID:             id,
						MissionID:      []byte("1234567890123456"),
						CoPilotID:      []byte("2345678901234567"),
						FromICAOID:     []byte("3456789012345678"),
						ToHospitalID:   []byte("4567890123456789"),
						FlightTakeOff:  time.Now().UTC(),
						FlightLanding:  time.Now().UTC(),
						FlightDuration: time.Hour,
						LandingCount:   1,
						Mission:        &missiondomain.Mission{Type: tc.typ},
						Status:         flightdata.StatusInProgress,
					}
					if got := flightdata.DeriveStatus(loaded, nil); got != flightdata.StatusCompleted {
						t.Fatalf("expected derived status complete, got %q", got)
					}
					return loaded, nil
				},
				updateFn: func(_ context.Context, row *flightdata.FlightData) error {
					updates = append(updates, row)
					return nil
				},
			}

			svc := NewFlightDataService(repo)
			if err := svc.syncStatus(context.Background(), rowID); err != nil {
				t.Fatalf("syncStatus returned error: %v", err)
			}
			if len(updates) != 1 {
				t.Fatalf("expected one status sync update, got %d", len(updates))
			}
			if updates[0].Status != flightdata.StatusCompleted {
				t.Fatalf("expected sync update to complete status, got %q", updates[0].Status)
			}
		})
	}
}

func TestFlightDataServiceSyncStatusFallsBackToMissionLookup(t *testing.T) {
	var updates []*flightdata.FlightData
	rowID := []byte("019ecc1133333333")
	missionID := []byte("1234567890123456")
	repo := &flightDataRepoStub{
		getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
			return &flightdata.FlightData{
				ID:              id,
				MissionID:       missionID,
				CoPilotID:       []byte("2345678901234567"),
				FromHospitalID:  []byte("3456789012345678"),
				ToHospitalID:    []byte("4567890123456789"),
				FlightTakeOff:   time.Now().UTC(),
				FlightLanding:   time.Now().UTC(),
				FlightDuration:  time.Hour,
				LandingCount:    1,
				RotorBrakeCycle: 1,
				Status:          flightdata.StatusInProgress,
			}, nil
		},
		getByMissionIDFn: func(_ context.Context, _ []byte) (*flightdata.FlightData, error) {
			return &flightdata.FlightData{
				ID:              rowID,
				MissionID:       missionID,
				CoPilotID:       []byte("2345678901234567"),
				FromHospitalID:  []byte("3456789012345678"),
				ToHospitalID:    []byte("4567890123456789"),
				FlightTakeOff:   time.Now().UTC(),
				FlightLanding:   time.Now().UTC(),
				FlightDuration:  time.Hour,
				LandingCount:    1,
				RotorBrakeCycle: 1,
				Mission:         &missiondomain.Mission{Type: sharedconst.MissionTypeCAT},
				Status:          flightdata.StatusInProgress,
			}, nil
		},
		updateFn: func(_ context.Context, row *flightdata.FlightData) error {
			updates = append(updates, row)
			return nil
		},
	}

	svc := NewFlightDataService(repo)
	if err := svc.syncStatus(context.Background(), rowID); err != nil {
		t.Fatalf("syncStatus returned error: %v", err)
	}
	if len(updates) != 1 {
		t.Fatalf("expected one status sync update, got %d", len(updates))
	}
	if updates[0].Status != flightdata.StatusCompleted {
		t.Fatalf("expected sync update to complete status, got %q", updates[0].Status)
	}
}

func TestFlightDataServiceCreateBlockedWhenFMReportExists(t *testing.T) {
	missionID := uuidv7.MustBytes()
	repo := &flightDataRepoStub{
		createFn: func(context.Context, *flightdata.FlightData) error {
			t.Fatal("create should not be called when fm report already exists")
			return nil
		},
	}
	svc := NewFlightDataService(repo).
		WithMissionResolver(missionResolverStub{flightID: uuidv7.MustBytes()}).
		WithFMReportLookup(fmReportLookupStub{report: &fmreport.Report{ID: uuidv7.MustBytes(), FlightID: uuidv7.MustBytes()}})

	err := svc.Create(context.Background(), &flightdata.FlightData{
		MissionID:       missionID,
		CoPilotID:       uuidv7.MustBytes(),
		FromHospitalID:  uuidv7.MustBytes(),
		ToICAOID:        uuidv7.MustBytes(),
		FlightTakeOff:   time.Now().UTC(),
		FlightLanding:   time.Now().UTC(),
		FlightDuration:  time.Hour,
		LandingCount:    1,
		RotorBrakeCycle: 1,
	})
	if !errors.Is(err, flightdata.ErrCreateBlocked) {
		t.Fatalf("expected create blocked, got %v", err)
	}
}

func TestFlightDataServiceUpdateLockedWhenFMReportCompleted(t *testing.T) {
	missionID := uuidv7.MustBytes()
	repo := &flightDataRepoStub{
		getByIDFn: func(context.Context, []byte) (*flightdata.FlightData, error) {
			return &flightdata.FlightData{
				ID:              uuidv7.MustBytes(),
				MissionID:       missionID,
				CoPilotID:       uuidv7.MustBytes(),
				FromHospitalID:  uuidv7.MustBytes(),
				ToICAOID:        uuidv7.MustBytes(),
				FlightTakeOff:   time.Now().UTC(),
				FlightLanding:   time.Now().UTC(),
				FlightDuration:  time.Hour,
				LandingCount:    1,
				RotorBrakeCycle: 1,
			}, nil
		},
		updateFn: func(context.Context, *flightdata.FlightData) error {
			t.Fatal("update should not be called when fm report is completed")
			return nil
		},
	}
	completedAt := time.Now().UTC()
	svc := NewFlightDataService(repo).
		WithMissionResolver(missionResolverStub{flightID: uuidv7.MustBytes()}).
		WithFMReportLookup(fmReportLookupStub{report: &fmreport.Report{ID: uuidv7.MustBytes(), FlightID: uuidv7.MustBytes(), CompletedAt: &completedAt}})

	err := svc.Update(context.Background(), &flightdata.FlightData{
		ID:              uuidv7.MustBytes(),
		MissionID:       missionID,
		CoPilotID:       uuidv7.MustBytes(),
		FromHospitalID:  uuidv7.MustBytes(),
		ToICAOID:        uuidv7.MustBytes(),
		FlightTakeOff:   time.Now().UTC(),
		FlightLanding:   time.Now().UTC(),
		FlightDuration:  time.Hour,
		LandingCount:    1,
		RotorBrakeCycle: 1,
	})
	if !errors.Is(err, flightdata.ErrLocked) {
		t.Fatalf("expected locked error, got %v", err)
	}
}

func TestFlightDataServiceCreateBlockedWhenAfterFlightInspectionExists(t *testing.T) {
	missionID := uuidv7.MustBytes()
	inspectionID := uuidv7.MustBytes()
	repo := &flightDataRepoStub{
		createFn: func(context.Context, *flightdata.FlightData) error {
			t.Fatal("create should not be called when after flight inspection exists")
			return nil
		},
	}
	svc := NewFlightDataService(repo).
		WithMissionResolver(missionResolverStub{flightID: uuidv7.MustBytes()}).
		WithFlightLookup(flightLookupStub{
			flight: &flightdomain.Flight{
				ID: uuidv7.MustBytes(),
				Takeover: &takeoverdomain.TakeoverAc{
					ReserveAc: &reserveac.ReserveAc{InspectionID: inspectionID},
				},
			},
		}).
		WithAfterFlightInspectionLookup(afterFlightInspectionLookupStub{row: &afterflightinspection.AfterFlightInspection{ID: uuidv7.MustBytes(), FlightInspectionID: inspectionID}})

	err := svc.Create(context.Background(), &flightdata.FlightData{
		MissionID:       missionID,
		CoPilotID:       uuidv7.MustBytes(),
		FromHospitalID:  uuidv7.MustBytes(),
		ToICAOID:        uuidv7.MustBytes(),
		FlightTakeOff:   time.Now().UTC(),
		FlightLanding:   time.Now().UTC(),
		FlightDuration:  time.Hour,
		LandingCount:    1,
		RotorBrakeCycle: 1,
	})
	if !errors.Is(err, flightdata.ErrLockedAfterFlightInspection) {
		t.Fatalf("expected after-flight inspection lock error, got %v", err)
	}
}

func TestFlightDataServiceUpdateAllowedWhenAfterFlightInspectionExists(t *testing.T) {
	missionID := uuidv7.MustBytes()
	inspectionID := uuidv7.MustBytes()
	updated := false
	repo := &flightDataRepoStub{
		getByIDFn: func(context.Context, []byte) (*flightdata.FlightData, error) {
			return &flightdata.FlightData{
				ID:              uuidv7.MustBytes(),
				MissionID:       missionID,
				CoPilotID:       uuidv7.MustBytes(),
				FromHospitalID:  uuidv7.MustBytes(),
				ToICAOID:        uuidv7.MustBytes(),
				FlightTakeOff:   time.Now().UTC(),
				FlightLanding:   time.Now().UTC(),
				FlightDuration:  time.Hour,
				LandingCount:    1,
				RotorBrakeCycle: 1,
			}, nil
		},
		updateFn: func(context.Context, *flightdata.FlightData) error {
			updated = true
			return nil
		},
	}
	svc := NewFlightDataService(repo).
		WithMissionResolver(missionResolverStub{flightID: uuidv7.MustBytes()}).
		WithFlightLookup(flightLookupStub{
			flight: &flightdomain.Flight{
				ID: uuidv7.MustBytes(),
				Takeover: &takeoverdomain.TakeoverAc{
					ReserveAc: &reserveac.ReserveAc{InspectionID: inspectionID},
				},
			},
		}).
		WithAfterFlightInspectionLookup(afterFlightInspectionLookupStub{row: &afterflightinspection.AfterFlightInspection{ID: uuidv7.MustBytes(), FlightInspectionID: inspectionID}})

	err := svc.Update(context.Background(), &flightdata.FlightData{
		ID:              uuidv7.MustBytes(),
		MissionID:       missionID,
		CoPilotID:       uuidv7.MustBytes(),
		FromHospitalID:  uuidv7.MustBytes(),
		ToICAOID:        uuidv7.MustBytes(),
		FlightTakeOff:   time.Now().UTC(),
		FlightLanding:   time.Now().UTC(),
		FlightDuration:  time.Hour,
		LandingCount:    1,
		RotorBrakeCycle: 1,
	})
	if err != nil {
		t.Fatalf("expected update to be allowed, got %v", err)
	}
	if !updated {
		t.Fatal("expected update to be called")
	}
}

func TestFlightDataServiceUpdateRequiresRow(t *testing.T) {
	svc := NewFlightDataService(&flightDataRepoStub{})
	if err := svc.Update(context.Background(), nil); !errors.Is(err, flightdata.ErrRequired) {
		t.Fatalf("expected ErrRequired, got %v", err)
	}
}
