package service

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

	afterflightinspection "wucher/internal/domain/after_flight_inspection"
	basedomain "wucher/internal/domain/base"
	flightdomain "wucher/internal/domain/flight"
	flightdata "wucher/internal/domain/flight_data"
	"wucher/internal/domain/mission"
	reserveac "wucher/internal/domain/reserve_ac"
	takeoverdomain "wucher/internal/domain/takeover"
	"wucher/internal/shared/pkg/util"
)

type missionRepoMock struct {
	createFn                 func(context.Context, *mission.Mission) error
	findCategoryByCodeFn     func(context.Context, string) (*mission.MissionCategory, error)
	updateFlightDataIDByIDFn func(context.Context, []byte, []byte, []byte) error
	deleteByIDFn             func(context.Context, []byte, []byte) error
	deleteByFlightIDFn       func(ctx context.Context, flightID []byte, deletedBy []byte) error
	maxCodeSeqFn             func() (int, error)
}

func (m *missionRepoMock) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
	return fn(ctx)
}

func (m *missionRepoMock) Create(ctx context.Context, row *mission.Mission) error {
	if m.createFn != nil {
		return m.createFn(ctx, row)
	}
	return nil
}

func (m *missionRepoMock) CreateCategory(context.Context, *mission.MissionCategory) error { return nil }

func (m *missionRepoMock) CreatePlaceholder(context.Context, *flightdata.FlightData) error {
	return nil
}

func (m *missionRepoMock) UpdateFlightDataIDByID(ctx context.Context, missionID, flightDataID, updatedBy []byte) error {
	if m.updateFlightDataIDByIDFn != nil {
		return m.updateFlightDataIDByIDFn(ctx, missionID, flightDataID, updatedBy)
	}
	return nil
}

func (m *missionRepoMock) UpdateByID(context.Context, []byte, string, []byte, []byte, string, []byte) error {
	return nil
}
func (m *missionRepoMock) AttachFile(context.Context, []byte, []byte) error {
	return nil
}

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

func (m *missionRepoMock) DeleteByID(ctx context.Context, missionID []byte, deletedBy []byte) error {
	if m.deleteByIDFn != nil {
		return m.deleteByIDFn(ctx, missionID, deletedBy)
	}
	return nil
}

func (m *missionRepoMock) DeleteByFlightID(ctx context.Context, flightID []byte, deletedBy []byte) error {
	if m.deleteByFlightIDFn != nil {
		return m.deleteByFlightIDFn(ctx, flightID, deletedBy)
	}
	return nil
}

func (m *missionRepoMock) ListByFlightID(context.Context, []byte) ([]mission.Mission, error) {
	return nil, nil
}
func (m *missionRepoMock) ListByFlightIDs(context.Context, [][]byte) ([]mission.Mission, error) {
	return nil, nil
}

func (m *missionRepoMock) GetByID(context.Context, []byte) (*mission.Mission, error) {
	return nil, nil
}

func (m *missionRepoMock) GetByFlightID(context.Context, []byte) (*mission.Mission, error) {
	return nil, nil
}

func (m *missionRepoMock) List(context.Context, string, string, []byte, string, int, int) ([]mission.Mission, int64, error) {
	return nil, 0, nil
}

func (m *missionRepoMock) ListDatatable(context.Context, mission.ListFilter, int, int) ([]mission.Mission, int64, int64, error) {
	return nil, 0, 0, nil
}

func (m *missionRepoMock) ListCategoriesWithSubCategories(context.Context) ([]mission.MissionCategory, error) {
	return nil, nil
}

func (m *missionRepoMock) FindCategoryByCode(ctx context.Context, code string) (*mission.MissionCategory, error) {
	if m.findCategoryByCodeFn != nil {
		return m.findCategoryByCodeFn(ctx, code)
	}
	return nil, nil
}

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

func (m *missionRepoMock) MaxCodeSeqByPrefix(context.Context, string) (int, error) {
	if m.maxCodeSeqFn != nil {
		return m.maxCodeSeqFn()
	}
	return 0, nil
}

type flightDataSvcMockForMission struct {
	createPlaceholderFn func(context.Context, *flightdata.FlightData) error
}

type afterFlightInspectionSvcMockForMission struct {
	getByFlightInspectionIDFn func(context.Context, []byte) (*afterflightinspection.AfterFlightInspection, error)
}

type flightSvcMockForMission struct {
	getByIDFn func(context.Context, []byte) (*flightdomain.Flight, error)
}

func (m *flightSvcMockForMission) Create(context.Context, *flightdomain.Flight) error { return nil }
func (m *flightSvcMockForMission) Update(context.Context, *flightdomain.Flight) error { return nil }
func (m *flightSvcMockForMission) Delete(context.Context, []byte, []byte) error       { return nil }
func (m *flightSvcMockForMission) GetByID(ctx context.Context, id []byte) (*flightdomain.Flight, error) {
	if m.getByIDFn != nil {
		return m.getByIDFn(ctx, id)
	}
	return nil, nil
}
func (m *flightSvcMockForMission) ListByUserID(context.Context, []byte, int, int) ([]flightdomain.Flight, int64, error) {
	return nil, 0, nil
}
func (m *flightSvcMockForMission) ListByCreatedBy(context.Context, []byte, string, string, string, int, int) ([]flightdomain.Flight, int64, error) {
	return nil, 0, nil
}
func (m *flightSvcMockForMission) GetByReserveAcID(context.Context, []byte) (*flightdomain.Flight, error) {
	return nil, nil
}
func (m *flightSvcMockForMission) ListByReserveAcID(context.Context, []byte) ([]flightdomain.Flight, error) {
	return nil, nil
}
func (m *flightSvcMockForMission) GetByTakeoverAcID(context.Context, []byte) (*flightdomain.Flight, error) {
	return nil, nil
}
func (m *flightSvcMockForMission) ListByTakeoverAcIDs(context.Context, [][]byte) ([]flightdomain.Flight, error) {
	return nil, nil
}
func (m *flightSvcMockForMission) GetByDutyRosterID(context.Context, []byte) (*flightdomain.Flight, error) {
	return nil, nil
}
func (m *flightSvcMockForMission) ListByDutyRosterIDs(context.Context, [][]byte) ([]flightdomain.Flight, error) {
	return nil, nil
}
func (m *flightSvcMockForMission) List(context.Context, string, string, string, int, int) ([]flightdomain.Flight, int64, error) {
	return nil, 0, nil
}

func (m *flightDataSvcMockForMission) Create(context.Context, *flightdata.FlightData) error {
	return nil
}

func (m *flightDataSvcMockForMission) CreatePlaceholder(ctx context.Context, row *flightdata.FlightData) error {
	if m.createPlaceholderFn != nil {
		return m.createPlaceholderFn(ctx, row)
	}
	return nil
}

func (m *flightDataSvcMockForMission) Update(context.Context, *flightdata.FlightData) error {
	return nil
}

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

func (m *flightDataSvcMockForMission) GetByID(context.Context, []byte) (*flightdata.FlightData, error) {
	return nil, nil
}

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

func (m *flightDataSvcMockForMission) GetByMissionID(context.Context, []byte) (*flightdata.FlightData, error) {
	return nil, nil
}

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

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

func (m *flightDataSvcMockForMission) GetSPODetailsByFlightDataID(context.Context, []byte) (*flightdata.SPODetails, error) {
	return nil, nil
}

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

func (m *afterFlightInspectionSvcMockForMission) Upsert(context.Context, []byte, *afterflightinspection.UpsertRequest) (*afterflightinspection.AfterFlightInspection, error) {
	return nil, nil
}

func (m *afterFlightInspectionSvcMockForMission) GetByFlightInspectionID(ctx context.Context, id []byte) (*afterflightinspection.AfterFlightInspection, error) {
	if m.getByFlightInspectionIDFn != nil {
		return m.getByFlightInspectionIDFn(ctx, id)
	}
	return nil, nil
}

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

func TestMissionServiceCreateDoesNotAutoCreateFlightData(t *testing.T) {
	var createdPlaceholder *flightdata.FlightData
	var updatedFlightDataID bool
	flightID := []byte("flight-id-123456")
	date := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
	baseRow := &basedomain.Base{
		ID:        []byte("base-id-12345678"),
		BaseName:  "Zurich",
		Latitude:  47.3769,
		Longitude: 8.5417,
		UTC:       "Europe/Zurich",
		OperationalShiftTimes: []basedomain.BaseOperationalShiftTime{{
			DateStart:     &date,
			DateEnd:       &date,
			StartTimeType: basedomain.ShiftTimeTypeFixed,
			EndTimeType:   basedomain.ShiftTimeTypeFixed,
			ShiftStart:    "09:00:00",
			ShiftEnd:      "17:00:00",
		}},
	}

	svc := NewMissionService(
		&missionRepoMock{
			findCategoryByCodeFn: func(context.Context, string) (*mission.MissionCategory, error) {
				return &mission.MissionCategory{ID: []byte("category-id-1234"), CodeType: "SPO", TypeName: "SPO"}, nil
			},
			updateFlightDataIDByIDFn: func(context.Context, []byte, []byte, []byte) error {
				updatedFlightDataID = true
				return nil
			},
			maxCodeSeqFn: func() (int, error) { return 4, nil },
			createFn: func(_ context.Context, row *mission.Mission) error {
				if row.StartTime == "" || row.EndTime == "" {
					t.Fatalf("expected resolved mission times on create, got start=%q end=%q", row.StartTime, row.EndTime)
				}
				if !strings.HasPrefix(row.Code, "SPO-") || !strings.HasSuffix(row.Code, "-5") {
					t.Fatalf("expected generated code SPO-<yy>-5, got %q", row.Code)
				}
				return nil
			},
		},
		&flightSvcMockForMission{
			getByIDFn: func(context.Context, []byte) (*flightdomain.Flight, error) {
				return &flightdomain.Flight{
					ID:   flightID,
					Date: date,
					Takeover: &takeoverdomain.TakeoverAc{
						Base: baseRow,
					},
				}, nil
			},
		},
		&flightDataSvcMockForMission{
			createPlaceholderFn: func(_ context.Context, row *flightdata.FlightData) error {
				row.ID = []byte("flight-data-id-123")
				createdPlaceholder = row
				return nil
			},
		},
	)

	row := &mission.Mission{
		FlightID:  flightID,
		Type:      "spo",
		CreatedBy: []byte("creator-id-12345"),
		UpdatedBy: []byte("creator-id-12345"),
	}

	if err := svc.Create(context.Background(), row); err != nil {
		t.Fatalf("Create returned error: %v", err)
	}
	// Mission create must NOT auto-create any flight data anymore.
	if createdPlaceholder != nil {
		t.Fatal("expected no placeholder flight data to be created on mission create")
	}
	if updatedFlightDataID {
		t.Fatal("expected mission flight_data_id not to be touched")
	}
	if len(row.FlightDataID) != 0 {
		t.Fatalf("expected mission flight_data_id to stay empty, got %x", row.FlightDataID)
	}
}

func TestMissionServiceCreateBlockedWhenAfterFlightInspectionExists(t *testing.T) {
	flightID := []byte("flight-id-123456")
	inspectionID := []byte("inspection-id-12")
	date := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
	baseRow := &basedomain.Base{
		ID:        []byte("base-id-12345678"),
		BaseName:  "Zurich",
		Latitude:  47.3769,
		Longitude: 8.5417,
		UTC:       "Europe/Zurich",
		OperationalShiftTimes: []basedomain.BaseOperationalShiftTime{{
			DateStart:     &date,
			DateEnd:       &date,
			StartTimeType: basedomain.ShiftTimeTypeFixed,
			EndTimeType:   basedomain.ShiftTimeTypeFixed,
			ShiftStart:    "09:00:00",
			ShiftEnd:      "17:00:00",
		}},
	}
	repo := &missionRepoMock{
		findCategoryByCodeFn: func(context.Context, string) (*mission.MissionCategory, error) {
			return &mission.MissionCategory{ID: []byte("category-id-1234"), CodeType: "SPO", TypeName: "SPO"}, nil
		},
		createFn: func(context.Context, *mission.Mission) error {
			t.Fatal("mission create should not be called when after flight inspection exists")
			return nil
		},
	}

	svc := NewMissionService(
		repo,
		&flightSvcMockForMission{
			getByIDFn: func(context.Context, []byte) (*flightdomain.Flight, error) {
				return &flightdomain.Flight{
					ID:   flightID,
					Date: date,
					Takeover: &takeoverdomain.TakeoverAc{
						Base:      baseRow,
						ReserveAc: &reserveac.ReserveAc{InspectionID: inspectionID},
					},
				}, nil
			},
		},
		&afterFlightInspectionSvcMockForMission{
			getByFlightInspectionIDFn: func(context.Context, []byte) (*afterflightinspection.AfterFlightInspection, error) {
				return &afterflightinspection.AfterFlightInspection{ID: []byte("after-flight-123")}, nil
			},
		},
	)

	err := svc.Create(context.Background(), &mission.Mission{
		FlightID:  flightID,
		Type:      "SPO",
		CreatedBy: []byte("creator-12345678"),
		UpdatedBy: []byte("creator-12345678"),
	})
	if !errors.Is(err, mission.ErrLockedAfterFlightInspection) {
		t.Fatalf("expected after-flight inspection lock error, got %v", err)
	}
}

func TestMissionServiceCreateResolvesMissionTimes(t *testing.T) {
	flightID := []byte("flight-id-123456")
	date := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)

	cases := []struct {
		name            string
		startType       string
		endType         string
		startClock      string
		endClock        string
		useDefaultShift bool
		defaultStart    string
		defaultEnd      string
		wantStart       string
		wantEnd         string
	}{
		{
			name:       "fixed",
			startType:  basedomain.ShiftTimeTypeFixed,
			endType:    basedomain.ShiftTimeTypeFixed,
			startClock: "06:00:00",
			endClock:   "21:00:00",
			wantStart:  "06:00",
			wantEnd:    "21:00",
		},
		{
			name:      "bmct and ecet",
			startType: basedomain.ShiftTimeTypeBMCT,
			endType:   basedomain.ShiftTimeTypeECET,
			wantStart: "04:49",
			wantEnd:   "22:05",
		},
		{
			name:       "mixed fixed start",
			startType:  basedomain.ShiftTimeTypeFixed,
			endType:    basedomain.ShiftTimeTypeECET,
			startClock: "23:59:00",
			wantStart:  "23:59",
			wantEnd:    "22:05",
		},
		{
			name:      "mixed fixed end",
			startType: basedomain.ShiftTimeTypeBMCT,
			endType:   basedomain.ShiftTimeTypeFixed,
			endClock:  "23:59:00",
			wantStart: "04:49",
			wantEnd:   "23:59",
		},
		{
			name:            "fallback to default shift when operational schedule missing",
			useDefaultShift: true,
			wantStart:       "06:00",
			wantEnd:         "21:00",
		},
		{
			name:            "fallback accepts datetime default shifts",
			useDefaultShift: true,
			defaultStart:    "2000-01-01 06:00:00",
			defaultEnd:      "2000-01-01 21:00:00",
			wantStart:       "06:00",
			wantEnd:         "21:00",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			baseRow := &basedomain.Base{
				ID:                []byte("base-id-12345678"),
				BaseName:          "Zurich",
				Latitude:          47.3769,
				Longitude:         8.5417,
				UTC:               "Europe/Zurich",
				DefaultShiftStart: "06:00:00",
				DefaultShiftEnd:   "21:00:00",
			}
			if tc.defaultStart != "" {
				baseRow.DefaultShiftStart = tc.defaultStart
			}
			if tc.defaultEnd != "" {
				baseRow.DefaultShiftEnd = tc.defaultEnd
			}
			if !tc.useDefaultShift {
				baseRow.OperationalShiftTimes = []basedomain.BaseOperationalShiftTime{{
					DateStart:     &date,
					DateEnd:       &date,
					StartTimeType: tc.startType,
					EndTimeType:   tc.endType,
					ShiftStart:    tc.startClock,
					ShiftEnd:      tc.endClock,
				}}
			}

			var gotStart, gotEnd string
			svc := NewMissionService(
				&missionRepoMock{
					findCategoryByCodeFn: func(context.Context, string) (*mission.MissionCategory, error) {
						return &mission.MissionCategory{ID: []byte("category-id-1234"), CodeType: "SPO", TypeName: "SPO"}, nil
					},
					createFn: func(_ context.Context, row *mission.Mission) error {
						gotStart = row.StartTime
						gotEnd = row.EndTime
						return nil
					},
				},
				&flightSvcMockForMission{
					getByIDFn: func(context.Context, []byte) (*flightdomain.Flight, error) {
						return &flightdomain.Flight{
							ID:   flightID,
							Date: date,
							Takeover: &takeoverdomain.TakeoverAc{
								Base: baseRow,
							},
						}, nil
					},
				},
			)

			row := &mission.Mission{FlightID: flightID, Type: "SPO"}
			if err := svc.Create(context.Background(), row); err != nil {
				t.Fatalf("Create returned error: %v", err)
			}
			if gotStart != tc.wantStart || gotEnd != tc.wantEnd {
				t.Fatalf("unexpected resolved times: got %s/%s want %s/%s", gotStart, gotEnd, tc.wantStart, tc.wantEnd)
			}
		})
	}
}

func TestMissionServiceDeleteByFlightIDRequiresFlightID(t *testing.T) {
	svc := NewMissionService(&missionRepoMock{})

	err := svc.DeleteByFlightID(context.Background(), nil, nil)
	if !errors.Is(err, mission.ErrFlightIDRequired) {
		t.Fatalf("expected ErrFlightIDRequired, got %v", err)
	}
}

func TestMissionServiceDeleteByFlightIDDelegatesToRepo(t *testing.T) {
	wantFlightID := []byte("1234567890123456")
	wantDeletedBy := []byte("6543210987654321")
	called := false

	svc := NewMissionService(&missionRepoMock{
		deleteByFlightIDFn: func(_ context.Context, flightID []byte, deletedBy []byte) error {
			called = true
			if string(flightID) != string(wantFlightID) {
				t.Fatalf("unexpected flightID: %x", flightID)
			}
			if string(deletedBy) != string(wantDeletedBy) {
				t.Fatalf("unexpected deletedBy: %x", deletedBy)
			}
			return nil
		},
	})

	if err := svc.DeleteByFlightID(context.Background(), wantFlightID, wantDeletedBy); err != nil {
		t.Fatalf("DeleteByFlightID returned error: %v", err)
	}
	if !called {
		t.Fatal("expected repository DeleteByFlightID to be called")
	}
}

func TestNormalizeMissionClockValueAcceptsDatetime(t *testing.T) {
	got := util.NormalizeClockValue("2000-01-01 08:00:00")
	if got != "08:00" {
		t.Fatalf("expected 08:00, got %q", got)
	}
}

func TestMissionServiceDeleteByIDRequiresMissionID(t *testing.T) {
	svc := NewMissionService(&missionRepoMock{})

	err := svc.DeleteByID(context.Background(), nil, nil)
	if !errors.Is(err, mission.ErrMissionIDRequired) {
		t.Fatalf("expected ErrMissionIDRequired, got %v", err)
	}
}

func TestMissionServiceDeleteByIDDelegatesToRepo(t *testing.T) {
	wantMissionID := []byte("1234567890123456")
	wantDeletedBy := []byte("6543210987654321")
	called := false

	svc := NewMissionService(&missionRepoMock{
		deleteByIDFn: func(_ context.Context, missionID []byte, deletedBy []byte) error {
			called = true
			if string(missionID) != string(wantMissionID) {
				t.Fatalf("unexpected missionID: %x", missionID)
			}
			if string(deletedBy) != string(wantDeletedBy) {
				t.Fatalf("unexpected deletedBy: %x", deletedBy)
			}
			return nil
		},
	})

	if err := svc.DeleteByID(context.Background(), wantMissionID, wantDeletedBy); err != nil {
		t.Fatalf("DeleteByID returned error: %v", err)
	}
	if !called {
		t.Fatal("expected repository DeleteByID to be called")
	}
}
