package service

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
	dto "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
	repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
	"gorm.io/gorm"
)

type expenseDepreciationRepoMock struct {
	repportRepo.ExpenseDepreciationRepository
	manualInputs  []repportRepo.FarmDepreciationManualInputRow
	candidateRows []repportRepo.FarmDepreciationCandidateRow
	snapshots     []entity.FarmDepreciationSnapshot

	upsertedRow         *entity.FarmDepreciationManualInput
	deleteCalled        bool
	deleteDate          time.Time
	deleteFarmIDs       []uint
	upsertSnapshotCalls int
	upsertedSnapshots   []entity.FarmDepreciationSnapshot
	getSnapshotsCalls   int
}

func (m *expenseDepreciationRepoMock) DB() *gorm.DB {
	return nil
}

func (m *expenseDepreciationRepoMock) UpsertManualInput(_ context.Context, row *entity.FarmDepreciationManualInput) error {
	if row == nil {
		return nil
	}
	cloned := *row
	if cloned.Id == 0 {
		cloned.Id = 123
	}
	m.upsertedRow = &cloned
	row.Id = cloned.Id
	return nil
}

func (m *expenseDepreciationRepoMock) GetCandidateFarms(_ context.Context, _ time.Time, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationCandidateRow, error) {
	return append([]repportRepo.FarmDepreciationCandidateRow{}, m.candidateRows...), nil
}

func (m *expenseDepreciationRepoMock) GetSnapshotsByPeriodAndFarmIDs(_ context.Context, period time.Time, farmIDs []uint) ([]entity.FarmDepreciationSnapshot, error) {
	m.getSnapshotsCalls++
	if len(farmIDs) == 0 {
		return []entity.FarmDepreciationSnapshot{}, nil
	}
	allowed := make(map[uint]struct{}, len(farmIDs))
	for _, farmID := range farmIDs {
		allowed[farmID] = struct{}{}
	}
	result := make([]entity.FarmDepreciationSnapshot, 0, len(m.snapshots))
	for _, row := range m.snapshots {
		if _, ok := allowed[row.ProjectFlockId]; !ok {
			continue
		}
		if row.PeriodDate.IsZero() || row.PeriodDate.Format("2006-01-02") == period.Format("2006-01-02") {
			result = append(result, row)
		}
	}
	return result, nil
}

func (m *expenseDepreciationRepoMock) UpsertSnapshots(_ context.Context, rows []entity.FarmDepreciationSnapshot) error {
	m.upsertSnapshotCalls++
	m.upsertedSnapshots = append([]entity.FarmDepreciationSnapshot{}, rows...)
	return nil
}

func (m *expenseDepreciationRepoMock) DeleteSnapshotsFromDate(_ context.Context, fromDate time.Time, farmIDs []uint) error {
	m.deleteCalled = true
	m.deleteDate = fromDate
	m.deleteFarmIDs = append([]uint{}, farmIDs...)
	return nil
}

func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Context, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationManualInputRow, error) {
	return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil
}

type expenseRealizationRepoMock struct {
	expenseRepo.ExpenseRealizationRepository
	db *gorm.DB
}

func (m *expenseRealizationRepoMock) DB() *gorm.DB {
	return m.db
}

type hppCostRepoMock struct {
	commonRepo.HppCostRepository
	kandangIDsByFarm map[uint][]uint
}

func (m *hppCostRepoMock) GetProjectFlockKandangIDs(_ context.Context, projectFlockID uint) ([]uint, error) {
	return append([]uint{}, m.kandangIDsByFarm[projectFlockID]...), nil
}

type hppV2ServiceMock struct {
	approvalService.HppV2Service
	breakdownByPFK map[uint]*approvalService.HppV2Breakdown
}

func (m *hppV2ServiceMock) CalculateHppBreakdown(projectFlockKandangId uint, _ *time.Time) (*approvalService.HppV2Breakdown, error) {
	return m.breakdownByPFK[projectFlockKandangId], nil
}

func TestComputeExpenseDepreciationSnapshots_FromHppV2NormalTransfer(t *testing.T) {
	periodDate := mustJakartaDate(t, "2026-06-05")
	svc := &repportService{
		HppCostRepo: &hppCostRepoMock{
			kandangIDsByFarm: map[uint][]uint{
				1: {10},
			},
		},
		HppV2Svc: &hppV2ServiceMock{
			breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{
				10: {
					ProjectFlockKandangID: 10,
					KandangID:             100,
					KandangName:           "Kandang A",
					HouseType:             "close_house",
					Components: []approvalService.HppV2Component{
						{
							Code:  "DEPRECIATION",
							Title: "Depreciation",
							Total: 100,
							Parts: []approvalService.HppV2ComponentPart{
								{
									Code:  "normal_transfer",
									Total: 100,
									Details: map[string]any{
										"schedule_day":            2,
										"depreciation_percent":    10.0,
										"pullet_cost_day_n":       1000.0,
										"source_project_flock_id": 77,
										"origin_date":             "2026-01-01",
									},
									References: []approvalService.HppV2Reference{
										{
											Type: "laying_transfer",
											ID:   701,
											Date: "2026-05-20",
											Qty:  150,
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}

	rows, err := svc.computeExpenseDepreciationSnapshots(context.Background(), periodDate, []uint{1}, map[uint]string{1: "Farm A"})
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("expected 1 row, got %d", len(rows))
	}
	if rows[0].DepreciationValue != 100 {
		t.Fatalf("expected depreciation value 100, got %v", rows[0].DepreciationValue)
	}
	if rows[0].PulletCostDayNTotal != 1000 {
		t.Fatalf("expected pullet cost day n 1000, got %v", rows[0].PulletCostDayNTotal)
	}
	assertFloatEqual(t, rows[0].DepreciationPercentEffective, 10)

	components := decodeDepreciationComponents(t, rows[0].Components)
	if components.KandangCount != 1 {
		t.Fatalf("expected kandang_count 1, got %d", components.KandangCount)
	}
	entry := components.Kandang[0]
	if entry.ProjectFlockKandangID != 10 || entry.KandangID != 100 || entry.KandangName != "Kandang A" {
		t.Fatalf("unexpected kandang identity: %+v", entry)
	}
	if entry.TransferID != 701 || entry.TransferDate != "2026-05-20" || entry.TransferQty != 150 {
		t.Fatalf("unexpected transfer metadata: %+v", entry)
	}
	if entry.DepreciationSource != "normal_transfer" {
		t.Fatalf("expected depreciation_source normal_transfer, got %q", entry.DepreciationSource)
	}
	if entry.ManualInputID != nil || entry.CutoverDate != "" || entry.StartScheduleDay != nil {
		t.Fatalf("expected manual fields empty for normal transfer, got %+v", entry)
	}
}

func TestComputeExpenseDepreciationSnapshots_FromHppV2ManualCutover(t *testing.T) {
	periodDate := mustJakartaDate(t, "2026-06-05")
	svc := &repportService{
		HppCostRepo: &hppCostRepoMock{
			kandangIDsByFarm: map[uint][]uint{
				2: {20},
			},
		},
		HppV2Svc: &hppV2ServiceMock{
			breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{
				20: {
					ProjectFlockKandangID: 20,
					KandangID:             200,
					KandangName:           "Kandang B",
					HouseType:             "open_house",
					Components: []approvalService.HppV2Component{
						{
							Code:  "DEPRECIATION",
							Title: "Depreciation",
							Total: 200,
							Parts: []approvalService.HppV2ComponentPart{
								{
									Code:  "manual_cutover",
									Total: 200,
									Details: map[string]any{
										"schedule_day":         2,
										"start_schedule_day":   2,
										"depreciation_percent": 25.0,
										"pullet_cost_day_n":    800.0,
										"manual_input_id":      901,
										"cutover_date":         "2026-06-01",
										"origin_date":          "2026-01-01",
									},
									References: []approvalService.HppV2Reference{
										{
											Type: "farm_depreciation_manual_input",
											ID:   901,
											Date: "2026-06-01",
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}

	rows, err := svc.computeExpenseDepreciationSnapshots(context.Background(), periodDate, []uint{2}, map[uint]string{2: "Farm B"})
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("expected 1 row, got %d", len(rows))
	}
	assertFloatEqual(t, rows[0].DepreciationPercentEffective, 25)

	components := decodeDepreciationComponents(t, rows[0].Components)
	if components.KandangCount != 1 {
		t.Fatalf("expected kandang_count 1, got %d", components.KandangCount)
	}
	entry := components.Kandang[0]
	if entry.DepreciationSource != "manual_cutover" {
		t.Fatalf("expected depreciation_source manual_cutover, got %q", entry.DepreciationSource)
	}
	if entry.TransferID != 0 || entry.TransferDate != "" || entry.TransferQty != 0 {
		t.Fatalf("expected transfer fields empty for manual path, got %+v", entry)
	}
	if entry.ManualInputID == nil || *entry.ManualInputID != 901 {
		t.Fatalf("expected manual_input_id 901, got %+v", entry.ManualInputID)
	}
	if entry.CutoverDate != "2026-06-01" || entry.OriginDate != "2026-01-01" {
		t.Fatalf("unexpected manual date fields: %+v", entry)
	}
	if entry.StartScheduleDay == nil || *entry.StartScheduleDay != 2 {
		t.Fatalf("expected start_schedule_day 2, got %+v", entry.StartScheduleDay)
	}
}

func TestComputeExpenseDepreciationSnapshots_AggregatesMultipleKandang(t *testing.T) {
	periodDate := mustJakartaDate(t, "2026-06-05")
	svc := &repportService{
		HppCostRepo: &hppCostRepoMock{
			kandangIDsByFarm: map[uint][]uint{
				3: {30, 31},
			},
		},
		HppV2Svc: &hppV2ServiceMock{
			breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{
				30: {
					ProjectFlockKandangID: 30,
					KandangID:             300,
					KandangName:           "Kandang C1",
					Components: []approvalService.HppV2Component{
						{
							Code: "DEPRECIATION",
							Parts: []approvalService.HppV2ComponentPart{
								{
									Code:  "normal_transfer",
									Total: 50,
									Details: map[string]any{
										"schedule_day":         1,
										"depreciation_percent": 10.0,
										"pullet_cost_day_n":    500.0,
									},
								},
							},
						},
					},
				},
				31: {
					ProjectFlockKandangID: 31,
					KandangID:             301,
					KandangName:           "Kandang C2",
					Components: []approvalService.HppV2Component{
						{
							Code: "DEPRECIATION",
							Parts: []approvalService.HppV2ComponentPart{
								{
									Code:  "normal_transfer",
									Total: 100,
									Details: map[string]any{
										"schedule_day":         2,
										"depreciation_percent": 10.0,
										"pullet_cost_day_n":    1000.0,
									},
								},
							},
						},
					},
				},
			},
		},
	}

	rows, err := svc.computeExpenseDepreciationSnapshots(context.Background(), periodDate, []uint{3}, map[uint]string{3: "Farm C"})
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("expected 1 row, got %d", len(rows))
	}
	if rows[0].DepreciationValue != 150 {
		t.Fatalf("expected depreciation value 150, got %v", rows[0].DepreciationValue)
	}
	if rows[0].PulletCostDayNTotal != 1500 {
		t.Fatalf("expected pullet cost day n 1500, got %v", rows[0].PulletCostDayNTotal)
	}
	assertFloatEqual(t, rows[0].DepreciationPercentEffective, 10)

	components := decodeDepreciationComponents(t, rows[0].Components)
	if components.KandangCount != 2 {
		t.Fatalf("expected kandang_count 2, got %d", components.KandangCount)
	}
}

func TestComputeExpenseDepreciationSnapshots_ZeroWhenDepreciationMissing(t *testing.T) {
	periodDate := mustJakartaDate(t, "2026-06-05")
	svc := &repportService{
		HppCostRepo: &hppCostRepoMock{
			kandangIDsByFarm: map[uint][]uint{
				4: {40},
			},
		},
		HppV2Svc: &hppV2ServiceMock{
			breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{
				40: {
					ProjectFlockKandangID: 40,
					KandangID:             400,
					KandangName:           "Kandang D",
					Components: []approvalService.HppV2Component{
						{Code: "PAKAN", Total: 123},
					},
				},
			},
		},
	}

	rows, err := svc.computeExpenseDepreciationSnapshots(context.Background(), periodDate, []uint{4}, map[uint]string{4: "Farm D"})
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("expected 1 row, got %d", len(rows))
	}
	if rows[0].DepreciationValue != 0 || rows[0].PulletCostDayNTotal != 0 || rows[0].DepreciationPercentEffective != 0 {
		t.Fatalf("expected zero snapshot values, got %+v", rows[0])
	}
	components := decodeDepreciationComponents(t, rows[0].Components)
	if components.KandangCount != 0 || len(components.Kandang) != 0 {
		t.Fatalf("expected empty components, got %+v", components)
	}
}

func TestGetExpenseDepreciation_UsesExistingSnapshotWhenForceRecomputeFalse(t *testing.T) {
	periodDate := mustJakartaDate(t, "2026-06-05")
	repo := &expenseDepreciationRepoMock{
		candidateRows: []repportRepo.FarmDepreciationCandidateRow{
			{ProjectFlockID: 1, FarmName: "Farm A"},
		},
		snapshots: []entity.FarmDepreciationSnapshot{
			{
				ProjectFlockId:               1,
				PeriodDate:                   periodDate,
				DepreciationPercentEffective: 11.1,
				DepreciationValue:            111,
				PulletCostDayNTotal:          1001,
				Components:                   json.RawMessage(`{"kandang_count":0,"kandang":[]}`),
			},
		},
	}

	svc := &repportService{
		Validate:                validator.New(),
		ExpenseDepreciationRepo: repo,
		ExpenseRealizationRepo:  &expenseRealizationRepoMock{},
		HppCostRepo:             &hppCostRepoMock{},
		HppV2Svc:                &hppV2ServiceMock{},
	}

	rows, meta, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("expected 1 row, got %d", len(rows))
	}
	if rows[0].DepreciationValue != 111 {
		t.Fatalf("expected depreciation value 111, got %v", rows[0].DepreciationValue)
	}
	if meta == nil || meta.TotalResults != 1 {
		t.Fatalf("expected meta total_results 1, got %+v", meta)
	}
	if repo.upsertSnapshotCalls != 0 {
		t.Fatalf("expected no snapshot upsert, got %d", repo.upsertSnapshotCalls)
	}
	if repo.getSnapshotsCalls != 1 {
		t.Fatalf("expected snapshot fetch called once, got %d", repo.getSnapshotsCalls)
	}
}

func TestGetExpenseDepreciation_ForceRecomputeRebuildsAllSnapshots(t *testing.T) {
	periodDate := mustJakartaDate(t, "2026-06-05")
	repo := &expenseDepreciationRepoMock{
		candidateRows: []repportRepo.FarmDepreciationCandidateRow{
			{ProjectFlockID: 1, FarmName: "Farm A"},
		},
		snapshots: []entity.FarmDepreciationSnapshot{
			{
				ProjectFlockId:      1,
				PeriodDate:          periodDate,
				DepreciationValue:   999,
				PulletCostDayNTotal: 999,
			},
		},
	}

	svc := &repportService{
		Validate:                validator.New(),
		ExpenseDepreciationRepo: repo,
		ExpenseRealizationRepo:  &expenseRealizationRepoMock{},
		HppCostRepo: &hppCostRepoMock{
			kandangIDsByFarm: map[uint][]uint{
				1: {10},
			},
		},
		HppV2Svc: &hppV2ServiceMock{
			breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{
				10: depreciationBreakdown(10, 100, "Kandang A", 100, 1000, 10),
			},
		},
	}

	rows, _, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05&force_recompute=true")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("expected 1 row, got %d", len(rows))
	}
	if rows[0].DepreciationValue != 100 {
		t.Fatalf("expected recomputed depreciation value 100, got %v", rows[0].DepreciationValue)
	}
	if repo.upsertSnapshotCalls != 1 {
		t.Fatalf("expected snapshot upsert called once, got %d", repo.upsertSnapshotCalls)
	}
	if len(repo.upsertedSnapshots) != 1 || repo.upsertedSnapshots[0].ProjectFlockId != 1 {
		t.Fatalf("expected upserted snapshot for farm 1, got %+v", repo.upsertedSnapshots)
	}
	if repo.getSnapshotsCalls != 0 {
		t.Fatalf("expected no snapshot fetch in force recompute mode, got %d", repo.getSnapshotsCalls)
	}
}

func TestGetExpenseDepreciation_ForceRecomputeFalseComputesOnlyMissingFarms(t *testing.T) {
	periodDate := mustJakartaDate(t, "2026-06-05")
	repo := &expenseDepreciationRepoMock{
		candidateRows: []repportRepo.FarmDepreciationCandidateRow{
			{ProjectFlockID: 1, FarmName: "Farm A"},
			{ProjectFlockID: 2, FarmName: "Farm B"},
		},
		snapshots: []entity.FarmDepreciationSnapshot{
			{
				ProjectFlockId:               1,
				PeriodDate:                   periodDate,
				DepreciationPercentEffective: 11.1,
				DepreciationValue:            111,
				PulletCostDayNTotal:          1001,
				Components:                   json.RawMessage(`{"kandang_count":0,"kandang":[]}`),
			},
		},
	}

	svc := &repportService{
		Validate:                validator.New(),
		ExpenseDepreciationRepo: repo,
		ExpenseRealizationRepo:  &expenseRealizationRepoMock{},
		HppCostRepo: &hppCostRepoMock{
			kandangIDsByFarm: map[uint][]uint{
				1: {10},
				2: {20},
			},
		},
		HppV2Svc: &hppV2ServiceMock{
			breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{
				10: depreciationBreakdown(10, 100, "Kandang A", 999, 9999, 10),
				20: depreciationBreakdown(20, 200, "Kandang B", 200, 2000, 10),
			},
		},
	}

	rows, _, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(rows) != 2 {
		t.Fatalf("expected 2 rows, got %d", len(rows))
	}
	if rows[0].ProjectFlockID != 1 || rows[0].DepreciationValue != 111 {
		t.Fatalf("expected farm 1 use existing snapshot value 111, got %+v", rows[0])
	}
	if rows[1].ProjectFlockID != 2 || rows[1].DepreciationValue != 200 {
		t.Fatalf("expected farm 2 recomputed value 200, got %+v", rows[1])
	}
	if repo.upsertSnapshotCalls != 1 {
		t.Fatalf("expected one upsert call for missing farms, got %d", repo.upsertSnapshotCalls)
	}
	if len(repo.upsertedSnapshots) != 1 || repo.upsertedSnapshots[0].ProjectFlockId != 2 {
		t.Fatalf("expected upsert only farm 2 snapshot, got %+v", repo.upsertedSnapshots)
	}
	if repo.getSnapshotsCalls != 1 {
		t.Fatalf("expected snapshot fetch called once, got %d", repo.getSnapshotsCalls)
	}
}

func TestUpsertExpenseDepreciationManualInput_InvalidatesSnapshotsFromCutoverDate(t *testing.T) {
	repo := &expenseDepreciationRepoMock{
		manualInputs: []repportRepo.FarmDepreciationManualInputRow{
			{
				Id:             123,
				ProjectFlockID: 99,
				FarmName:       "Farm Z",
				TotalCost:      1000,
				CutoverDate:    mustJakartaDate(t, "2026-06-01"),
			},
		},
	}

	svc := &repportService{
		Validate:                validator.New(),
		ExpenseDepreciationRepo: repo,
	}

	reqPayload := &validation.ExpenseDepreciationManualInputUpsert{
		ProjectFlockID: 99,
		TotalCost:      1000,
		CutoverDate:    "2026-06-01",
	}

	app := fiber.New()
	var response *dto.ExpenseDepreciationManualInputRowDTO
	app.Put("/", func(c *fiber.Ctx) error {
		result, err := svc.UpsertExpenseDepreciationManualInput(c, reqPayload)
		if err != nil {
			return err
		}
		response = result
		return c.SendStatus(fiber.StatusOK)
	})

	httpResp, err := app.Test(httptest.NewRequest(http.MethodPut, "/", nil))
	if err != nil {
		t.Fatalf("expected no app error, got %v", err)
	}
	if httpResp.StatusCode != fiber.StatusOK {
		t.Fatalf("expected status %d, got %d", fiber.StatusOK, httpResp.StatusCode)
	}
	if !repo.deleteCalled {
		t.Fatal("expected DeleteSnapshotsFromDate to be called")
	}
	if len(repo.deleteFarmIDs) != 1 || repo.deleteFarmIDs[0] != 99 {
		t.Fatalf("expected delete farm ids [99], got %v", repo.deleteFarmIDs)
	}
	if repo.deleteDate.Format("2006-01-02") != "2026-06-01" {
		t.Fatalf("expected delete date 2026-06-01, got %s", repo.deleteDate.Format("2006-01-02"))
	}
	if response == nil {
		t.Fatal("expected response")
	}
	if response.FarmName != "Farm Z" {
		t.Fatalf("expected farm name Farm Z, got %s", response.FarmName)
	}
}

func getExpenseDepreciationByQuery(t *testing.T, svc *repportService, query string) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error) {
	t.Helper()

	app := fiber.New()
	var (
		rows []dto.ExpenseDepreciationRowDTO
		meta *dto.ExpenseDepreciationMetaDTO
	)
	app.Get("/", func(c *fiber.Ctx) error {
		resultRows, resultMeta, err := svc.GetExpenseDepreciation(c)
		if err != nil {
			return err
		}
		rows = resultRows
		meta = resultMeta
		return c.SendStatus(fiber.StatusOK)
	})

	target := "/"
	if query != "" {
		target += "?" + query
	}
	resp, err := app.Test(httptest.NewRequest(http.MethodGet, target, nil))
	if err != nil {
		return nil, nil, err
	}
	if resp.StatusCode != fiber.StatusOK {
		return nil, nil, fiber.NewError(resp.StatusCode, "request failed")
	}
	return rows, meta, nil
}

func depreciationBreakdown(
	projectFlockKandangID uint,
	kandangID uint,
	kandangName string,
	depreciationValue float64,
	pulletCostDayN float64,
	depreciationPercent float64,
) *approvalService.HppV2Breakdown {
	return &approvalService.HppV2Breakdown{
		ProjectFlockKandangID: projectFlockKandangID,
		KandangID:             kandangID,
		KandangName:           kandangName,
		HouseType:             "close_house",
		Components: []approvalService.HppV2Component{
			{
				Code:  "DEPRECIATION",
				Title: "Depreciation",
				Total: depreciationValue,
				Parts: []approvalService.HppV2ComponentPart{
					{
						Code:  "normal_transfer",
						Total: depreciationValue,
						Details: map[string]any{
							"schedule_day":            1,
							"depreciation_percent":    depreciationPercent,
							"pullet_cost_day_n":       pulletCostDayN,
							"source_project_flock_id": 77,
							"origin_date":             "2026-01-01",
						},
						References: []approvalService.HppV2Reference{
							{
								Type: "laying_transfer",
								ID:   701,
								Date: "2026-05-20",
								Qty:  100,
							},
						},
					},
				},
			},
		},
	}
}

func decodeDepreciationComponents(t *testing.T, raw []byte) depreciationFarmComponents {
	t.Helper()
	var out depreciationFarmComponents
	if len(raw) == 0 {
		return out
	}
	if err := json.Unmarshal(raw, &out); err != nil {
		t.Fatalf("failed to decode components: %v", err)
	}
	return out
}

func mustJakartaDate(t *testing.T, raw string) time.Time {
	t.Helper()
	location, err := time.LoadLocation("Asia/Jakarta")
	if err != nil {
		t.Fatalf("failed loading timezone: %v", err)
	}
	value, err := time.ParseInLocation("2006-01-02", raw, location)
	if err != nil {
		t.Fatalf("failed parsing date %q: %v", raw, err)
	}
	return value
}

func assertFloatEqual(t *testing.T, got float64, want float64) {
	t.Helper()
	const epsilon = 0.000001
	if got > want+epsilon || got < want-epsilon {
		t.Fatalf("expected %.6f, got %.6f", want, got)
	}
}
