package service

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"testing"
	"time"

	commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
)

type hppV2RepoStub struct {
	contextByPFK         map[uint]*commonRepo.HppV2ProjectFlockKandangContext
	pfkIDsByProject      map[uint][]uint
	latestTransferByPFK  map[uint]*commonRepo.HppV2LatestTransferInputRow
	manualInputByProject map[uint]*commonRepo.HppV2ManualDepreciationInputRow
	snapshotByProjectKey map[string]*commonRepo.HppV2FarmDepreciationSnapshotRow
	chickInDateByProject map[uint]*time.Time
	depreciationByHouse  map[string]map[int]float64
	usageRowsByKey       map[string][]commonRepo.HppV2UsageCostRow
	adjustRowsByKey      map[string][]commonRepo.HppV2AdjustmentCostRow
	chickinRowsByKey     map[string][]commonRepo.HppV2ChickinCostRow
	expenseRowsByPFKKey  map[string][]commonRepo.HppV2ExpenseCostRow
	expenseRowsByFarmKey map[string][]commonRepo.HppV2ExpenseCostRow
	routeCostByProject   map[uint]float64
	totalPopulationByKey map[string]float64
	transferSummaryByPFK map[uint]struct {
		projectFlockID uint
		totalQty       float64
	}
	eggProductionByPFK map[uint]struct {
		pieces float64
		kg     float64
	}
	eggSalesByPFK map[uint]struct {
		pieces float64
		kg     float64
	}
}

func (s *hppV2RepoStub) GetProjectFlockKandangContext(_ context.Context, projectFlockKandangId uint) (*commonRepo.HppV2ProjectFlockKandangContext, error) {
	row := s.contextByPFK[projectFlockKandangId]
	if row == nil {
		return nil, fmt.Errorf("pfk %d not found", projectFlockKandangId)
	}
	return row, nil
}

func (s *hppV2RepoStub) GetProjectFlockKandangIDs(_ context.Context, projectFlockId uint) ([]uint, error) {
	return append([]uint{}, s.pfkIDsByProject[projectFlockId]...), nil
}

func (s *hppV2RepoStub) GetLatestTransferInputByProjectFlockKandangID(_ context.Context, projectFlockKandangId uint, _ time.Time) (*commonRepo.HppV2LatestTransferInputRow, error) {
	return s.latestTransferByPFK[projectFlockKandangId], nil
}

func (s *hppV2RepoStub) GetManualDepreciationInputByProjectFlockID(_ context.Context, projectFlockID uint) (*commonRepo.HppV2ManualDepreciationInputRow, error) {
	return s.manualInputByProject[projectFlockID], nil
}

func (s *hppV2RepoStub) GetRecordingStockRoutingAdjustmentCostByProjectFlockID(_ context.Context, projectFlockID uint, _ time.Time) (float64, error) {
	return s.routeCostByProject[projectFlockID], nil
}

func (s *hppV2RepoStub) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(_ context.Context, projectFlockID uint, periodDate time.Time) (*commonRepo.HppV2FarmDepreciationSnapshotRow, error) {
	if s.snapshotByProjectKey == nil {
		return nil, nil
	}
	return s.snapshotByProjectKey[fmt.Sprintf("%d|%s", projectFlockID, periodDate.Format("2006-01-02"))], nil
}

func (s *hppV2RepoStub) GetEarliestChickInDateByProjectFlockID(_ context.Context, projectFlockID uint) (*time.Time, error) {
	return s.chickInDateByProject[projectFlockID], nil
}

func (s *hppV2RepoStub) GetDepreciationPercents(_ context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error) {
	result := make(map[string]map[int]float64)
	for _, houseType := range houseTypes {
		source := s.depreciationByHouse[houseType]
		if len(source) == 0 {
			continue
		}
		result[houseType] = make(map[int]float64)
		for day, pct := range source {
			if day <= maxDay {
				result[houseType][day] = pct
			}
		}
	}
	return result, nil
}

func (s *hppV2RepoStub) ListUsageCostRowsByProductFlags(_ context.Context, projectFlockKandangIDs []uint, flagNames []string, _ *time.Time) ([]commonRepo.HppV2UsageCostRow, error) {
	return append([]commonRepo.HppV2UsageCostRow{}, s.usageRowsByKey[stubKey(projectFlockKandangIDs, flagNames)]...), nil
}

func (s *hppV2RepoStub) ListAdjustmentCostRowsByProductFlags(_ context.Context, projectFlockKandangIDs []uint, flagNames []string, _ *time.Time) ([]commonRepo.HppV2AdjustmentCostRow, error) {
	return append([]commonRepo.HppV2AdjustmentCostRow{}, s.adjustRowsByKey[stubKey(projectFlockKandangIDs, flagNames)]...), nil
}

func (s *hppV2RepoStub) ListExpenseRealizationRowsByProjectFlockKandangIDs(_ context.Context, projectFlockKandangIDs []uint, _ *time.Time, ekspedisi bool) ([]commonRepo.HppV2ExpenseCostRow, error) {
	return append([]commonRepo.HppV2ExpenseCostRow{}, s.expenseRowsByPFKKey[expenseStubKey(projectFlockKandangIDs, ekspedisi)]...), nil
}

func (s *hppV2RepoStub) ListExpenseRealizationRowsByProjectFlockID(_ context.Context, projectFlockID uint, _ *time.Time, ekspedisi bool) ([]commonRepo.HppV2ExpenseCostRow, error) {
	return append([]commonRepo.HppV2ExpenseCostRow{}, s.expenseRowsByFarmKey[expenseFarmKey(projectFlockID, ekspedisi)]...), nil
}

func (s *hppV2RepoStub) ListChickinCostRowsByProductFlags(_ context.Context, projectFlockKandangIDs []uint, flagNames []string, _ *time.Time, excludeTransferToLaying bool) ([]commonRepo.HppV2ChickinCostRow, error) {
	return append([]commonRepo.HppV2ChickinCostRow{}, s.chickinRowsByKey[chickinStubKey(projectFlockKandangIDs, flagNames, excludeTransferToLaying)]...), nil
}

func (s *hppV2RepoStub) GetFeedUsageCost(_ context.Context, _ []uint, _ *time.Time) (float64, error) {
	return 0, nil
}

func (s *hppV2RepoStub) GetTotalPopulation(_ context.Context, projectFlockKandangIDs []uint) (float64, error) {
	return s.totalPopulationByKey[stubKey(projectFlockKandangIDs, nil)], nil
}

func (s *hppV2RepoStub) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(_ context.Context, projectFlockKandangIDs []uint, _ *time.Time) (float64, float64, error) {
	totalPieces := 0.0
	totalKg := 0.0
	for _, projectFlockKandangID := range projectFlockKandangIDs {
		row := s.eggProductionByPFK[projectFlockKandangID]
		totalPieces += row.pieces
		totalKg += row.kg
	}
	return totalPieces, totalKg, nil
}

func (s *hppV2RepoStub) GetEggProduksiBreakdownByProjectFlockKandangIds(_ context.Context, projectFlockKandangIDs []uint, _ *time.Time) (float64, float64, float64, float64, error) {
	totalPieces := 0.0
	totalKg := 0.0
	for _, projectFlockKandangID := range projectFlockKandangIDs {
		row := s.eggProductionByPFK[projectFlockKandangID]
		totalPieces += row.pieces
		totalKg += row.kg
	}
	return totalPieces, totalKg, 0, 0, nil
}

func (s *hppV2RepoStub) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(_ context.Context, projectFlockKandangIDs []uint, _ *time.Time, _ *time.Time) (float64, float64, error) {
	if len(projectFlockKandangIDs) != 1 {
		return 0, 0, nil
	}
	row := s.eggSalesByPFK[projectFlockKandangIDs[0]]
	return row.pieces, row.kg, nil
}

func (s *hppV2RepoStub) GetTransferSourceSummary(_ context.Context, projectFlockKandangId uint) (uint, float64, error) {
	row := s.transferSummaryByPFK[projectFlockKandangId]
	return row.projectFlockID, row.totalQty, nil
}

func TestHppV2CalculateHppBreakdown_ComposesPakanSlices(t *testing.T) {
	repo := &hppV2RepoStub{
		contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
			10: {
				ProjectFlockKandangID: 10,
				ProjectFlockID:        2,
				ProjectFlockCategory:  "LAYING",
				KandangID:             100,
				KandangName:           "Kandang A",
				LocationID:            16,
			},
		},
		pfkIDsByProject: map[uint][]uint{
			1: {101, 102},
		},
		usageRowsByKey: map[string][]commonRepo.HppV2UsageCostRow{
			stubKey([]uint{101, 102}, []string{"PAKAN"}): {
				{StockableType: "purchase_items", StockableID: 9001, SourceProductID: 8, SourceProductName: "Pakan Growing", Qty: 100, UnitPrice: 40, TotalCost: 4000},
			},
			stubKey([]uint{10}, []string{"PAKAN"}): {
				{StockableType: "purchase_items", StockableID: 9002, SourceProductID: 9, SourceProductName: "Pakan Laying", Qty: 50, UnitPrice: 30, TotalCost: 1500},
			},
		},
		adjustRowsByKey: map[string][]commonRepo.HppV2AdjustmentCostRow{
			stubKey([]uint{101, 102}, []string{"PAKAN-CUTOVER"}): {
				{AdjustmentID: 8001, ProductID: 11, ProductName: "Pakan Growing Cut-over", Qty: 20, Price: 30, GrandTotal: 600},
			},
			stubKey([]uint{10}, []string{"PAKAN-CUTOVER"}): {
				{AdjustmentID: 8002, ProductID: 12, ProductName: "Pakan Laying Cut-over", Qty: 10, Price: 30, GrandTotal: 300},
			},
		},
		totalPopulationByKey: map[string]float64{
			stubKey([]uint{101, 102}, nil): 1000,
		},
		transferSummaryByPFK: map[uint]struct {
			projectFlockID uint
			totalQty       float64
		}{
			10: {projectFlockID: 1, totalQty: 250},
		},
		eggProductionByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			10: {pieces: 100, kg: 10},
		},
		eggSalesByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			10: {pieces: 40, kg: 4},
		},
	}

	svc := NewHppV2Service(repo)
	result, err := svc.CalculateHppBreakdown(10, mustDate(t, "2026-04-19"))
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if result == nil {
		t.Fatal("expected breakdown result")
	}
	if got := result.TotalPulletCost; got != 1150 {
		t.Fatalf("expected total pullet cost 1150, got %v", got)
	}
	if got := result.TotalProductionCost; got != 1800 {
		t.Fatalf("expected total production cost 1800, got %v", got)
	}
	if len(result.Components) != 1 {
		t.Fatalf("expected 1 component, got %d", len(result.Components))
	}
	component := result.Components[0]
	if component.Code != "PAKAN" {
		t.Fatalf("expected PAKAN component, got %s", component.Code)
	}
	partTotals := map[string]float64{}
	for _, part := range component.Parts {
		partTotals[part.Code] = part.Total
	}
	if partTotals[hppV2PartGrowingNormal] != 1000 {
		t.Fatalf("expected growing normal 1000, got %v", partTotals[hppV2PartGrowingNormal])
	}
	if partTotals[hppV2PartGrowingCutover] != 150 {
		t.Fatalf("expected growing cutover 150, got %v", partTotals[hppV2PartGrowingCutover])
	}
	if partTotals[hppV2PartLayingNormal] != 1500 {
		t.Fatalf("expected laying normal 1500, got %v", partTotals[hppV2PartLayingNormal])
	}
	if partTotals[hppV2PartLayingCutover] != 300 {
		t.Fatalf("expected laying cutover 300, got %v", partTotals[hppV2PartLayingCutover])
	}
	if component.Parts[0].Proration == nil || component.Parts[0].Proration.Ratio != 0.25 {
		t.Fatalf("expected growing proration ratio 0.25, got %+v", component.Parts[0].Proration)
	}
	if result.Hpp.Estimation.HargaKg != 180 {
		t.Fatalf("expected estimation harga/kg 180, got %v", result.Hpp.Estimation.HargaKg)
	}
	if result.Hpp.Real.HargaKg != 450 {
		t.Fatalf("expected real harga/kg 450, got %v", result.Hpp.Real.HargaKg)
	}
}

func TestHppV2CalculateHppBreakdown_ManualCutoverUsesLayingSlicesOnly(t *testing.T) {
	repo := &hppV2RepoStub{
		contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
			20: {
				ProjectFlockKandangID: 20,
				ProjectFlockID:        3,
				ProjectFlockCategory:  "LAYING",
				KandangID:             200,
				KandangName:           "Kandang B",
				LocationID:            17,
			},
		},
		usageRowsByKey: map[string][]commonRepo.HppV2UsageCostRow{
			stubKey([]uint{20}, []string{"PAKAN"}): {
				{StockableType: "purchase_items", StockableID: 9100, SourceProductID: 21, SourceProductName: "Pakan Laying", Qty: 20, UnitPrice: 10, TotalCost: 200},
			},
		},
		adjustRowsByKey: map[string][]commonRepo.HppV2AdjustmentCostRow{
			stubKey([]uint{20}, []string{"PAKAN-CUTOVER"}): {
				{AdjustmentID: 8100, ProductID: 22, ProductName: "Pakan Laying Cut-over", Qty: 30, Price: 10, GrandTotal: 300},
			},
		},
		eggProductionByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			20: {pieces: 50, kg: 5},
		},
		eggSalesByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			20: {pieces: 25, kg: 2.5},
		},
	}

	svc := NewHppV2Service(repo)
	result, err := svc.CalculateHppBreakdown(20, mustDate(t, "2026-04-19"))
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if result.TotalProductionCost != 500 {
		t.Fatalf("expected total production cost 500, got %v", result.TotalProductionCost)
	}
	component := result.Components[0]
	if len(component.Parts) != 2 {
		t.Fatalf("expected 2 laying parts, got %d", len(component.Parts))
	}
	for _, part := range component.Parts {
		if strings.HasPrefix(part.Code, "growing_") {
			t.Fatalf("expected no growing parts, got %s", part.Code)
		}
	}
	if result.Hpp.Estimation.HargaKg != 100 {
		t.Fatalf("expected estimation harga/kg 100, got %v", result.Hpp.Estimation.HargaKg)
	}
}

func TestHppV2CalculateHppBreakdown_IncludesOvkComponent(t *testing.T) {
	repo := &hppV2RepoStub{
		contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
			30: {
				ProjectFlockKandangID: 30,
				ProjectFlockID:        4,
				ProjectFlockCategory:  "LAYING",
				KandangID:             300,
				KandangName:           "Kandang C",
				LocationID:            18,
			},
		},
		pfkIDsByProject: map[uint][]uint{
			5: {301, 302},
		},
		usageRowsByKey: map[string][]commonRepo.HppV2UsageCostRow{
			stubKey([]uint{30}, []string{"PAKAN"}): {
				{StockableType: "purchase_items", StockableID: 9200, SourceProductID: 31, SourceProductName: "Pakan Laying", Qty: 20, UnitPrice: 25, TotalCost: 500},
			},
			stubKey([]uint{301, 302}, []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)}): {
				{StockableType: "purchase_items", StockableID: 9201, SourceProductID: 32, SourceProductName: "OVK Growing", Qty: 40, UnitPrice: 10, TotalCost: 400},
			},
			stubKey([]uint{30}, []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)}): {
				{StockableType: "purchase_items", StockableID: 9202, SourceProductID: 33, SourceProductName: "OVK Laying", Qty: 15, UnitPrice: 10, TotalCost: 150},
			},
		},
		adjustRowsByKey: map[string][]commonRepo.HppV2AdjustmentCostRow{
			stubKey([]uint{301, 302}, []string{"OVK"}): {
				{AdjustmentID: 8201, ProductID: 34, ProductName: "OVK Growing Cut-over", Qty: 10, Price: 10, GrandTotal: 100},
			},
			stubKey([]uint{30}, []string{"OVK"}): {
				{AdjustmentID: 8202, ProductID: 35, ProductName: "OVK Laying Cut-over", Qty: 5, Price: 10, GrandTotal: 50},
			},
		},
		totalPopulationByKey: map[string]float64{
			stubKey([]uint{301, 302}, nil): 1000,
		},
		transferSummaryByPFK: map[uint]struct {
			projectFlockID uint
			totalQty       float64
		}{
			30: {projectFlockID: 5, totalQty: 500},
		},
		eggProductionByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			30: {pieces: 120, kg: 12},
		},
		eggSalesByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			30: {pieces: 60, kg: 6},
		},
	}

	svc := NewHppV2Service(repo)
	result, err := svc.CalculateHppBreakdown(30, mustDate(t, "2026-04-19"))
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if result == nil {
		t.Fatal("expected breakdown result")
	}
	if len(result.Components) != 2 {
		t.Fatalf("expected 2 components, got %d", len(result.Components))
	}

	componentTotals := map[string]float64{}
	for _, component := range result.Components {
		componentTotals[component.Code] = component.Total
	}

	if componentTotals[hppV2ComponentPakan] != 500 {
		t.Fatalf("expected pakan total 500, got %v", componentTotals[hppV2ComponentPakan])
	}
	if componentTotals[hppV2ComponentOvk] != 450 {
		t.Fatalf("expected ovk total 450, got %v", componentTotals[hppV2ComponentOvk])
	}
	if result.TotalPulletCost != 250 {
		t.Fatalf("expected total pullet cost 250, got %v", result.TotalPulletCost)
	}
	if result.TotalProductionCost != 700 {
		t.Fatalf("expected total production cost 700, got %v", result.TotalProductionCost)
	}
	if result.Hpp.Estimation.HargaKg != 58.33 {
		t.Fatalf("expected estimation harga/kg 58.33, got %v", result.Hpp.Estimation.HargaKg)
	}
}

func TestHppV2CalculateHppBreakdown_IncludesDocAndDirectPulletChickin(t *testing.T) {
	repo := &hppV2RepoStub{
		contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
			35: {
				ProjectFlockKandangID: 35,
				ProjectFlockID:        8,
				ProjectFlockCategory:  "LAYING",
				KandangID:             350,
				KandangName:           "Kandang E",
				LocationID:            20,
			},
		},
		pfkIDsByProject: map[uint][]uint{
			9: {901, 902},
		},
		totalPopulationByKey: map[string]float64{
			stubKey([]uint{901, 902}, nil): 1000,
		},
		transferSummaryByPFK: map[uint]struct {
			projectFlockID uint
			totalQty       float64
		}{
			35: {projectFlockID: 9, totalQty: 250},
		},
		chickinRowsByKey: map[string][]commonRepo.HppV2ChickinCostRow{
			chickinStubKey([]uint{901, 902}, []string{string(utils.FlagDOC)}, false): {
				{ProjectChickinID: 1, ProjectFlockKandangID: 901, ChickInDate: mustTime(t, "2026-04-01"), StockableType: "purchase_items", StockableID: 1001, SourceProductID: 77, SourceProductName: "DOC", Qty: 1000, UnitPrice: 2, TotalCost: 2000},
			},
			chickinStubKey([]uint{35}, []string{string(utils.FlagPullet), string(utils.FlagLayer)}, true): {
				{ProjectChickinID: 2, ProjectFlockKandangID: 35, ChickInDate: mustTime(t, "2026-04-15"), StockableType: "purchase_items", StockableID: 1002, SourceProductID: 78, SourceProductName: "Pullet", Qty: 50, UnitPrice: 20, TotalCost: 1000},
			},
		},
		eggProductionByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			35: {pieces: 100, kg: 10},
		},
		eggSalesByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			35: {pieces: 80, kg: 8},
		},
	}

	svc := NewHppV2Service(repo)
	result, err := svc.CalculateHppBreakdown(35, mustDate(t, "2026-04-19"))
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}

	componentTotals := map[string]float64{}
	for _, component := range result.Components {
		componentTotals[component.Code] = component.Total
	}

	if componentTotals[hppV2ComponentDocChickin] != 500 {
		t.Fatalf("expected doc chickin total 500, got %v", componentTotals[hppV2ComponentDocChickin])
	}
	if componentTotals[hppV2ComponentDirectPulletPurchase] != 1000 {
		t.Fatalf("expected direct pullet purchase total 1000, got %v", componentTotals[hppV2ComponentDirectPulletPurchase])
	}
	if result.TotalPulletCost != 500 {
		t.Fatalf("expected total pullet cost 500, got %v", result.TotalPulletCost)
	}
	if result.TotalProductionCost != 1000 {
		t.Fatalf("expected total production cost 1000, got %v", result.TotalProductionCost)
	}
	if result.Hpp.Estimation.HargaKg != 100 {
		t.Fatalf("expected estimation harga/kg 100, got %v", result.Hpp.Estimation.HargaKg)
	}
}

func TestHppV2CalculateHppBreakdown_IncludesBopRegularAndEkspedisi(t *testing.T) {
	repo := &hppV2RepoStub{
		contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
			40: {
				ProjectFlockKandangID: 40,
				ProjectFlockID:        6,
				ProjectFlockCategory:  "LAYING",
				KandangID:             400,
				KandangName:           "Kandang D",
				LocationID:            19,
			},
		},
		pfkIDsByProject: map[uint][]uint{
			6: {40, 41},
			7: {701, 702},
		},
		totalPopulationByKey: map[string]float64{
			stubKey([]uint{701, 702}, nil): 1000,
		},
		transferSummaryByPFK: map[uint]struct {
			projectFlockID uint
			totalQty       float64
		}{
			40: {projectFlockID: 7, totalQty: 200},
		},
		expenseRowsByPFKKey: map[string][]commonRepo.HppV2ExpenseCostRow{
			expenseStubKey([]uint{701, 702}, false): {
				{ExpenseRealizationID: 1, NonstockID: 11, NonstockName: "Growing BOP", Qty: 1, Price: 500, TotalCost: 500, RealizationDate: mustTime(t, "2026-04-10")},
			},
			expenseStubKey([]uint{40}, false): {
				{ExpenseRealizationID: 2, NonstockID: 12, NonstockName: "Laying BOP", Qty: 1, Price: 80, TotalCost: 80, RealizationDate: mustTime(t, "2026-04-19")},
			},
			expenseStubKey([]uint{701, 702}, true): {
				{ExpenseRealizationID: 3, NonstockID: 13, NonstockName: "Growing Expedition", Qty: 1, Price: 100, TotalCost: 100, RealizationDate: mustTime(t, "2026-04-11")},
			},
			expenseStubKey([]uint{40}, true): {
				{ExpenseRealizationID: 4, NonstockID: 14, NonstockName: "Laying Expedition", Qty: 1, Price: 40, TotalCost: 40, RealizationDate: mustTime(t, "2026-04-19")},
			},
		},
		expenseRowsByFarmKey: map[string][]commonRepo.HppV2ExpenseCostRow{
			expenseFarmKey(7, false): {
				{ExpenseRealizationID: 5, NonstockID: 15, NonstockName: "Growing Farm BOP", Qty: 1, Price: 300, TotalCost: 300, RealizationDate: mustTime(t, "2026-04-12")},
			},
			expenseFarmKey(6, false): {
				{ExpenseRealizationID: 6, NonstockID: 16, NonstockName: "Laying Farm BOP", Qty: 1, Price: 100, TotalCost: 100, RealizationDate: mustTime(t, "2026-04-19")},
			},
			expenseFarmKey(7, true): {
				{ExpenseRealizationID: 7, NonstockID: 17, NonstockName: "Growing Farm Expedition", Qty: 1, Price: 50, TotalCost: 50, RealizationDate: mustTime(t, "2026-04-12")},
			},
			expenseFarmKey(6, true): {
				{ExpenseRealizationID: 8, NonstockID: 18, NonstockName: "Laying Farm Expedition", Qty: 1, Price: 60, TotalCost: 60, RealizationDate: mustTime(t, "2026-04-19")},
			},
		},
		eggProductionByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			40: {pieces: 30, kg: 3},
			41: {pieces: 70, kg: 7},
		},
		eggSalesByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			40: {pieces: 50, kg: 5},
		},
	}

	svc := NewHppV2Service(repo)
	result, err := svc.CalculateHppBreakdown(40, mustDate(t, "2026-04-19"))
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}

	componentTotals := map[string]float64{}
	for _, component := range result.Components {
		componentTotals[component.Code] = component.Total
	}

	if componentTotals[hppV2ComponentBopRegular] != 270 {
		t.Fatalf("expected regular BOP total 270, got %v", componentTotals[hppV2ComponentBopRegular])
	}
	if componentTotals[hppV2ComponentBopEksp] != 88 {
		t.Fatalf("expected expedition BOP total 88, got %v", componentTotals[hppV2ComponentBopEksp])
	}
	if result.TotalPulletCost != 190 {
		t.Fatalf("expected total pullet cost 190, got %v", result.TotalPulletCost)
	}
	if result.TotalProductionCost != 168 {
		t.Fatalf("expected total production cost 168, got %v", result.TotalProductionCost)
	}
	if result.Hpp.Estimation.HargaKg != 56 {
		t.Fatalf("expected estimation harga/kg 56, got %v", result.Hpp.Estimation.HargaKg)
	}
}

func TestHppV2CalculateHppBreakdown_AddsDepreciationForNormalTransfer(t *testing.T) {
	sourceChickIn := mustTime(t, "2026-01-01")
	reportDate := sourceChickIn.AddDate(0, 0, 154)

	repo := &hppV2RepoStub{
		contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
			50: {
				ProjectFlockKandangID: 50,
				ProjectFlockID:        10,
				ProjectFlockCategory:  "LAYING",
				KandangID:             500,
				KandangName:           "Kandang F",
				LocationID:            21,
				HouseType:             "close_house",
			},
		},
		pfkIDsByProject: map[uint][]uint{
			11: {501},
		},
		latestTransferByPFK: map[uint]*commonRepo.HppV2LatestTransferInputRow{
			50: {
				ProjectFlockKandangID: 50,
				SourceProjectFlockID:  11,
				TransferDate:          mustTime(t, "2026-05-20"),
				TransferQty:           100,
				TransferID:            701,
			},
		},
		chickInDateByProject: map[uint]*time.Time{
			11: &sourceChickIn,
		},
		depreciationByHouse: map[string]map[int]float64{
			"close_house": {
				1: 10,
			},
		},
		usageRowsByKey: map[string][]commonRepo.HppV2UsageCostRow{
			stubKey([]uint{501}, []string{"PAKAN"}): {
				{StockableType: "purchase_items", StockableID: 9301, SourceProductID: 41, SourceProductName: "Pakan Growing", Qty: 25, UnitPrice: 40, TotalCost: 1000},
			},
		},
		totalPopulationByKey: map[string]float64{
			stubKey([]uint{501}, nil): 100,
		},
		transferSummaryByPFK: map[uint]struct {
			projectFlockID uint
			totalQty       float64
		}{
			50: {projectFlockID: 11, totalQty: 100},
		},
		eggProductionByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			50: {pieces: 20, kg: 10},
		},
	}

	svc := NewHppV2Service(repo)
	result, err := svc.CalculateHppBreakdown(50, &reportDate)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}

	if result.TotalPulletCost != 1000 {
		t.Fatalf("expected total pullet cost 1000, got %v", result.TotalPulletCost)
	}
	if result.TotalProductionCost != 100 {
		t.Fatalf("expected total production cost 100, got %v", result.TotalProductionCost)
	}

	var depreciation *HppV2Component
	for i := range result.Components {
		if result.Components[i].Code == hppV2ComponentDepreciation {
			depreciation = &result.Components[i]
			break
		}
	}
	if depreciation == nil {
		t.Fatal("expected depreciation component")
	}
	if depreciation.Total != 100 {
		t.Fatalf("expected depreciation total 100, got %v", depreciation.Total)
	}
	if len(depreciation.Parts) != 1 {
		t.Fatalf("expected single depreciation part, got %d", len(depreciation.Parts))
	}
	if depreciation.Parts[0].Details["schedule_day"] != 1 {
		t.Fatalf("expected schedule day 1, got %+v", depreciation.Parts[0].Details)
	}
	if depreciation.Parts[0].Details["origin_date"] != "2026-01-01" {
		t.Fatalf("expected origin date 2026-01-01, got %+v", depreciation.Parts[0].Details)
	}
	if result.Hpp.Estimation.HargaKg != 10 {
		t.Fatalf("expected estimation harga/kg 10, got %v", result.Hpp.Estimation.HargaKg)
	}
}

func TestHppV2CalculateHppBreakdown_AddsDepreciationForManualCutoverFromCutoverDate(t *testing.T) {
	originDate := mustTime(t, "2026-01-01")
	cutoverDate := originDate.AddDate(0, 0, 155)

	repo := &hppV2RepoStub{
		contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
			60: {
				ProjectFlockKandangID: 60,
				ProjectFlockID:        12,
				ProjectFlockCategory:  "LAYING",
				KandangID:             600,
				KandangName:           "Kandang G",
				LocationID:            22,
				HouseType:             "close_house",
			},
		},
		pfkIDsByProject: map[uint][]uint{
			12: {60},
		},
		manualInputByProject: map[uint]*commonRepo.HppV2ManualDepreciationInputRow{
			12: {
				ID:             801,
				ProjectFlockID: 12,
				TotalCost:      1000,
				CutoverDate:    cutoverDate,
			},
		},
		chickInDateByProject: map[uint]*time.Time{
			12: &originDate,
		},
		depreciationByHouse: map[string]map[int]float64{
			"close_house": {
				1: 10,
				2: 20,
			},
		},
		totalPopulationByKey: map[string]float64{
			stubKey([]uint{60}, nil): 100,
		},
		eggProductionByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			60: {pieces: 20, kg: 10},
		},
	}

	svc := NewHppV2Service(repo)
	result, err := svc.CalculateHppBreakdown(60, &cutoverDate)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}

	if result.TotalPulletCost != 1000 {
		t.Fatalf("expected total pullet cost 1000, got %v", result.TotalPulletCost)
	}
	if result.TotalProductionCost != 200 {
		t.Fatalf("expected total production cost 200, got %v", result.TotalProductionCost)
	}

	componentTotals := map[string]float64{}
	for _, component := range result.Components {
		componentTotals[component.Code] = component.Total
	}
	if componentTotals[hppV2ComponentManualPulletCost] != 1000 {
		t.Fatalf("expected manual pullet cost 1000, got %v", componentTotals[hppV2ComponentManualPulletCost])
	}
	if componentTotals[hppV2ComponentDepreciation] != 200 {
		t.Fatalf("expected depreciation 200, got %v", componentTotals[hppV2ComponentDepreciation])
	}

	var depreciation *HppV2Component
	for i := range result.Components {
		if result.Components[i].Code == hppV2ComponentDepreciation {
			depreciation = &result.Components[i]
			break
		}
	}
	if depreciation == nil || len(depreciation.Parts) != 1 {
		t.Fatalf("expected one depreciation part, got %+v", depreciation)
	}
	if depreciation.Parts[0].Details["schedule_day"] != 2 {
		t.Fatalf("expected schedule day 2, got %+v", depreciation.Parts[0].Details)
	}
	if depreciation.Parts[0].Details["start_schedule_day"] != 2 {
		t.Fatalf("expected start schedule day 2, got %+v", depreciation.Parts[0].Details)
	}
	if result.Hpp.Estimation.HargaKg != 20 {
		t.Fatalf("expected estimation harga/kg 20, got %v", result.Hpp.Estimation.HargaKg)
	}
}

func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggProduction(t *testing.T) {
	reportDate := mustTime(t, "2026-06-05")

	repo := &hppV2RepoStub{
		contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
			70: {
				ProjectFlockKandangID: 70,
				ProjectFlockID:        15,
				ProjectFlockCategory:  "LAYING",
				KandangID:             700,
				KandangName:           "Kandang Snapshot",
				LocationID:            25,
				HouseType:             "close_house",
			},
		},
		pfkIDsByProject: map[uint][]uint{
			15: {70, 71},
		},
		snapshotByProjectKey: map[string]*commonRepo.HppV2FarmDepreciationSnapshotRow{
			"15|2026-06-05": {
				ID:                           901,
				ProjectFlockID:               15,
				PeriodDate:                   reportDate,
				DepreciationPercentEffective: 10,
				DepreciationValue:            1000,
				PulletCostDayNTotal:          10000,
			},
		},
		eggProductionByPFK: map[uint]struct {
			pieces float64
			kg     float64
		}{
			70: {pieces: 200, kg: 20},
			71: {pieces: 800, kg: 80},
		},
	}

	svc := NewHppV2Service(repo)
	result, err := svc.CalculateHppBreakdown(70, &reportDate)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if result == nil {
		t.Fatal("expected breakdown result")
	}

	var depreciation *HppV2Component
	for i := range result.Components {
		if result.Components[i].Code == hppV2ComponentDepreciation {
			depreciation = &result.Components[i]
			break
		}
	}
	if depreciation == nil {
		t.Fatal("expected depreciation component")
	}
	if depreciation.Total != 200 {
		t.Fatalf("expected depreciation total 200, got %v", depreciation.Total)
	}
	if result.TotalProductionCost != 200 {
		t.Fatalf("expected total production cost 200, got %v", result.TotalProductionCost)
	}
	if len(depreciation.Parts) != 1 {
		t.Fatalf("expected one depreciation part, got %d", len(depreciation.Parts))
	}
	if depreciation.Parts[0].Code != hppV2PartDepreciationFarmSnapshot {
		t.Fatalf("expected farm snapshot depreciation part, got %s", depreciation.Parts[0].Code)
	}
	if depreciation.Parts[0].Proration == nil || depreciation.Parts[0].Proration.Ratio != 0.2 {
		t.Fatalf("expected proration ratio 0.2, got %+v", depreciation.Parts[0].Proration)
	}
	if depreciation.Parts[0].Details["snapshot_id"] != uint(901) {
		t.Fatalf("expected snapshot id 901, got %+v", depreciation.Parts[0].Details)
	}
}

func stubKey(ids []uint, flags []string) string {
	idParts := make([]string, 0, len(ids))
	for _, id := range ids {
		idParts = append(idParts, fmt.Sprintf("%d", id))
	}
	sort.Strings(idParts)

	flagParts := append([]string{}, flags...)
	sort.Strings(flagParts)

	return strings.Join(idParts, ",") + "|" + strings.Join(flagParts, ",")
}

func mustDate(t *testing.T, raw string) *time.Time {
	t.Helper()
	loc, err := time.LoadLocation("Asia/Jakarta")
	if err != nil {
		t.Fatalf("failed to load timezone: %v", err)
	}
	value, err := time.ParseInLocation("2006-01-02", raw, loc)
	if err != nil {
		t.Fatalf("failed to parse date %s: %v", raw, err)
	}
	return &value
}

func mustTime(t *testing.T, raw string) time.Time {
	t.Helper()
	value := mustDate(t, raw)
	return *value
}

func expenseStubKey(ids []uint, ekspedisi bool) string {
	return stubKey(ids, []string{fmt.Sprintf("ekspedisi=%t", ekspedisi)})
}

func expenseFarmKey(projectFlockID uint, ekspedisi bool) string {
	return fmt.Sprintf("farm=%d|ekspedisi=%t", projectFlockID, ekspedisi)
}

func chickinStubKey(ids []uint, flags []string, excludeTransferToLaying bool) string {
	return stubKey(ids, append(append([]string{}, flags...), fmt.Sprintf("exclude_transfer_to_laying=%t", excludeTransferToLaying)))
}
