package service

import (
	"context"
	"testing"
	"time"

	"wucher/internal/domain/flight"
	fmreport "wucher/internal/domain/fm_report"
	"wucher/internal/shared/pkg/appctx"
	"wucher/internal/shared/pkg/uuidv7"
)

type fmReportRepoMock struct {
	upsertFn             func(context.Context, *fmreport.Report) error
	getByFlightFn        func(context.Context, []byte) (*fmreport.Report, error)
	maxReportSeqFn       func(context.Context, []byte) (int, error)
	setReportCodeFn      func(context.Context, []byte, string) error
	listFn               func(context.Context, fmreport.ListFilter, int, int) ([]fmreport.Report, int64, error)
	createFleetHistoryFn func(context.Context, *fmreport.FleetHistory) error
	getFleetHistoryFn    func(context.Context, []byte) (*fmreport.FleetHistory, error)
}

type fmReportUsageRefresherStub struct {
	calls int
}

func (s *fmReportUsageRefresherStub) RefreshAll(context.Context) error {
	s.calls++
	return nil
}

func (m *fmReportRepoMock) MaxReportSeqByHelicopter(ctx context.Context, helicopterID []byte) (int, error) {
	if m.maxReportSeqFn != nil {
		return m.maxReportSeqFn(ctx, helicopterID)
	}
	return 0, nil
}

func (m *fmReportRepoMock) SetReportCode(ctx context.Context, id []byte, code string) error {
	if m.setReportCodeFn != nil {
		return m.setReportCodeFn(ctx, id, code)
	}
	return nil
}

func fmStrPtr(v string) *string { return &v }

func (m *fmReportRepoMock) CreateFleetHistory(ctx context.Context, row *fmreport.FleetHistory) error {
	if m.createFleetHistoryFn != nil {
		return m.createFleetHistoryFn(ctx, row)
	}
	return nil
}

func (m *fmReportRepoMock) GetFleetHistoryByReportID(ctx context.Context, reportID []byte) (*fmreport.FleetHistory, error) {
	if m.getFleetHistoryFn != nil {
		return m.getFleetHistoryFn(ctx, reportID)
	}
	return nil, nil
}

func (m *fmReportRepoMock) Upsert(ctx context.Context, row *fmreport.Report) error {
	if m.upsertFn != nil {
		return m.upsertFn(ctx, row)
	}
	return nil
}

func (m *fmReportRepoMock) GetByFlightID(ctx context.Context, flightID []byte) (*fmreport.Report, error) {
	if m.getByFlightFn != nil {
		return m.getByFlightFn(ctx, flightID)
	}
	return nil, nil
}

func (m *fmReportRepoMock) List(ctx context.Context, filter fmreport.ListFilter, limit, offset int) ([]fmreport.Report, int64, error) {
	if m.listFn != nil {
		return m.listFn(ctx, filter, limit, offset)
	}
	return nil, 0, nil
}

func TestFMReportServiceUpsertForFlightPreservesExistingMetrics(t *testing.T) {
	flightID := uuidv7.MustBytes()
	takeoverID := uuidv7.MustBytes()
	inspectionID := uuidv7.MustBytes()
	existingID := uuidv7.MustBytes()
	userID := uuidv7.MustBytes()
	refresher := &fmReportUsageRefresherStub{}

	repo := &fmReportRepoMock{
		getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
			return &fmreport.Report{
				ID:                 existingID,
				FlightID:           flightID,
				TakeoverID:         takeoverID,
				FlightInspectionID: inspectionID,
				Engine1GpcN1:       fmStrPtr("old-e1-gpc"),
				Engine1PtcN2:       fmStrPtr("old-e1-ptc"),
				Engine2GpcN1:       fmStrPtr("old-e2-gpc"),
				Engine2PtcN2:       fmStrPtr("old-e2-ptc"),
				CreatedAt:          time.Date(2026, 6, 18, 10, 0, 0, 0, time.UTC),
				CreatedBy:          userID,
			}, nil
		},
		upsertFn: func(_ context.Context, row *fmreport.Report) error {
			if row.Engine1GpcN1 == nil || row.Engine1PtcN2 == nil || row.Engine2GpcN1 == nil || row.Engine2PtcN2 == nil {
				t.Fatalf("expected metrics preserved, got %+v", row)
			}
			if *row.Engine1GpcN1 != "old-e1-gpc" || *row.Engine1PtcN2 != "old-e1-ptc" || *row.Engine2GpcN1 != "old-e2-gpc" || *row.Engine2PtcN2 != "old-e2-ptc" {
				t.Fatalf("expected metrics preserved, got %+v", row)
			}
			return nil
		},
	}
	svc := NewFMReportService(repo).WithUsageRefresher(refresher)
	ctx := appctx.WithUserID(context.Background(), userID)

	got, err := svc.UpsertForFlight(ctx, &flight.Flight{ID: flightID, TakeoverAcID: takeoverID}, inspectionID)
	if err != nil {
		t.Fatalf("UpsertForFlight: %v", err)
	}
	if got == nil || got.Engine1GpcN1 == nil || got.Engine1PtcN2 == nil || got.Engine2GpcN1 == nil || got.Engine2PtcN2 == nil {
		t.Fatalf("unexpected report: %+v", got)
	}
	if *got.Engine1GpcN1 != "old-e1-gpc" || *got.Engine1PtcN2 != "old-e1-ptc" || *got.Engine2GpcN1 != "old-e2-gpc" || *got.Engine2PtcN2 != "old-e2-ptc" {
		t.Fatalf("unexpected report: %+v", got)
	}
	if refresher.calls != 0 {
		t.Fatalf("expected no usage refresh on upsert, got %d", refresher.calls)
	}
}

func TestFMReportServiceUpdateForFlight(t *testing.T) {
	flightID := uuidv7.MustBytes()
	reportID := uuidv7.MustBytes()
	userID := uuidv7.MustBytes()
	refresher := &fmReportUsageRefresherStub{}

	repo := &fmReportRepoMock{
		getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
			return &fmreport.Report{
				ID:                 reportID,
				FlightID:           flightID,
				TakeoverID:         uuidv7.MustBytes(),
				FlightInspectionID: uuidv7.MustBytes(),
				Engine1GpcN1:       fmStrPtr("old-e1-gpc"),
				Engine1PtcN2:       fmStrPtr("old-e1-ptc"),
				Engine2GpcN1:       fmStrPtr("old-e2-gpc"),
				Engine2PtcN2:       fmStrPtr("old-e2-ptc"),
			}, nil
		},
		upsertFn: func(ctx context.Context, row *fmreport.Report) error {
			if row.Engine1GpcN1 == nil || row.Engine1PtcN2 == nil || row.Engine2GpcN1 == nil || row.Engine2PtcN2 == nil {
				t.Fatalf("unexpected patched values: %+v", row)
			}
			if *row.Engine1GpcN1 != "new-e1-gpc" || *row.Engine1PtcN2 != "new-e1-ptc" || *row.Engine2GpcN1 != "new-e2-gpc" || *row.Engine2PtcN2 != "new-e2-ptc" {
				t.Fatalf("unexpected patched values: %+v", row)
			}
			if got := appctx.GetUserID(ctx); string(got) != string(userID) {
				t.Fatalf("expected updated by user to be preserved in context")
			}
			return nil
		},
	}
	svc := NewFMReportService(repo).WithUsageRefresher(refresher)
	ctx := appctx.WithUserID(context.Background(), userID)

	got, err := svc.UpdateForFlight(ctx, flightID, fmStrPtr("new-e1-gpc"), fmStrPtr("new-e1-ptc"), fmStrPtr("new-e2-gpc"), fmStrPtr("new-e2-ptc"))
	if err != nil {
		t.Fatalf("UpdateForFlight: %v", err)
	}
	if got == nil || got.Engine1GpcN1 == nil || got.Engine1PtcN2 == nil || got.Engine2GpcN1 == nil || got.Engine2PtcN2 == nil {
		t.Fatalf("unexpected report: %+v", got)
	}
	if *got.Engine1GpcN1 != "new-e1-gpc" || *got.Engine1PtcN2 != "new-e1-ptc" || *got.Engine2GpcN1 != "new-e2-gpc" || *got.Engine2PtcN2 != "new-e2-ptc" {
		t.Fatalf("unexpected report: %+v", got)
	}
	if refresher.calls != 0 {
		t.Fatalf("expected no usage refresh on update, got %d", refresher.calls)
	}
}

type fmReportCodeResolverMock struct {
	helicopterID   []byte
	helicopterType string
}

func (m fmReportCodeResolverMock) ResolveHelicopterForReportCode(context.Context, []byte) ([]byte, string) {
	return m.helicopterID, m.helicopterType
}

func TestFMReportServiceUpsertForFlightAssignsReportCode(t *testing.T) {
	flightID := uuidv7.MustBytes()
	takeoverID := uuidv7.MustBytes()
	inspectionID := uuidv7.MustBytes()
	helicopterID := uuidv7.MustBytes()
	userID := uuidv7.MustBytes()

	var saved *fmreport.Report
	var setCode string
	repo := &fmReportRepoMock{
		getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
			return nil, nil
		},
		maxReportSeqFn: func(_ context.Context, gotHeli []byte) (int, error) {
			if string(gotHeli) != string(helicopterID) {
				t.Fatalf("expected counter scoped to helicopter, got %x", gotHeli)
			}
			return 4, nil
		},
		upsertFn: func(_ context.Context, row *fmreport.Report) error {
			saved = row
			if string(row.HelicopterID) != string(helicopterID) {
				t.Fatalf("expected helicopter id persisted, got %x", row.HelicopterID)
			}
			if row.ReportCode != nil {
				t.Fatalf("expected report code unset on insert, got %q", *row.ReportCode)
			}
			if len(row.ID) != 16 {
				row.ID = uuidv7.MustBytes()
			}
			return nil
		},
		setReportCodeFn: func(_ context.Context, _ []byte, code string) error {
			setCode = code
			return nil
		},
	}
	svc := NewFMReportService(repo).WithReportCodeResolver(fmReportCodeResolverMock{
		helicopterID:   helicopterID,
		helicopterType: "SN0901",
	})
	ctx := appctx.WithUserID(context.Background(), userID)

	got, err := svc.UpsertForFlight(ctx, &flight.Flight{ID: flightID, TakeoverAcID: takeoverID}, inspectionID)
	if err != nil {
		t.Fatalf("UpsertForFlight: %v", err)
	}
	if saved == nil {
		t.Fatalf("expected report upserted")
	}
	if setCode != "SN0901-0005" {
		t.Fatalf("expected report code SN0901-0005 persisted, got %q", setCode)
	}
	if got == nil || got.ReportCode == nil || *got.ReportCode != "SN0901-0005" {
		t.Fatalf("unexpected returned report: %+v", got)
	}
}

func TestFMReportServiceUpsertForFlightRetriesOnDuplicateReportCode(t *testing.T) {
	flightID := uuidv7.MustBytes()
	takeoverID := uuidv7.MustBytes()
	inspectionID := uuidv7.MustBytes()
	helicopterID := uuidv7.MustBytes()

	maxCalls := 0
	setCalls := 0
	repo := &fmReportRepoMock{
		getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
			return nil, nil
		},
		maxReportSeqFn: func(context.Context, []byte) (int, error) {
			// First read sees 4; after the concurrent winner committed SN0901-0005,
			// the retry sees 5.
			maxCalls++
			if maxCalls == 1 {
				return 4, nil
			}
			return 5, nil
		},
		setReportCodeFn: func(_ context.Context, _ []byte, code string) error {
			setCalls++
			if code == "SN0901-0005" {
				return fmreport.ErrDuplicateReportCode
			}
			return nil
		},
		upsertFn: func(_ context.Context, row *fmreport.Report) error {
			if len(row.ID) != 16 {
				row.ID = uuidv7.MustBytes()
			}
			return nil
		},
	}
	svc := NewFMReportService(repo).WithReportCodeResolver(fmReportCodeResolverMock{
		helicopterID:   helicopterID,
		helicopterType: "SN0901",
	})

	got, err := svc.UpsertForFlight(context.Background(), &flight.Flight{ID: flightID, TakeoverAcID: takeoverID}, inspectionID)
	if err != nil {
		t.Fatalf("UpsertForFlight: %v", err)
	}
	if setCalls != 2 {
		t.Fatalf("expected one retry after duplicate, got %d SetReportCode calls", setCalls)
	}
	if got == nil || got.ReportCode == nil || *got.ReportCode != "SN0901-0006" {
		t.Fatalf("expected report code SN0901-0006 after retry, got %+v", got)
	}
}

func TestFMReportServiceUpdateForFlightNotFound(t *testing.T) {
	svc := NewFMReportService(&fmReportRepoMock{
		getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
			return nil, nil
		},
	})
	if _, err := svc.UpdateForFlight(context.Background(), uuidv7.MustBytes(), fmStrPtr("a"), fmStrPtr("b"), fmStrPtr("c"), fmStrPtr("d")); err == nil {
		t.Fatal("expected error")
	}
}

func TestFMReportServiceCompleteSetsCompletionFieldsAndRefreshesUsage(t *testing.T) {
	flightID := uuidv7.MustBytes()
	reportID := uuidv7.MustBytes()
	userID := uuidv7.MustBytes()
	refresher := &fmReportUsageRefresherStub{}

	repo := &fmReportRepoMock{
		getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
			return &fmreport.Report{
				ID:                 reportID,
				FlightID:           flightID,
				TakeoverID:         uuidv7.MustBytes(),
				FlightInspectionID: uuidv7.MustBytes(),
			}, nil
		},
		upsertFn: func(ctx context.Context, row *fmreport.Report) error {
			if row.CompletedAt == nil {
				t.Fatal("expected completed_at to be set")
			}
			if len(row.CompletedBy) != 16 {
				t.Fatal("expected completed_by to be set")
			}
			if got := appctx.GetUserID(ctx); string(got) != string(userID) {
				t.Fatal("expected user id propagated in context")
			}
			return nil
		},
	}
	svc := NewFMReportService(repo).WithUsageRefresher(refresher)
	ctx := appctx.WithUserID(context.Background(), userID)

	got, err := svc.Complete(ctx, flightID)
	if err != nil {
		t.Fatalf("Complete: %v", err)
	}
	if got == nil || got.CompletedAt == nil || len(got.CompletedBy) != 16 {
		t.Fatalf("unexpected completed report: %+v", got)
	}
	if refresher.calls != 1 {
		t.Fatalf("expected one usage refresh, got %d", refresher.calls)
	}
}
