package aggregation

import (
	"encoding/json"
	"os"
	"testing"
	"time"

	"git.toki-labs.com/toki/alt/packages/domain/market"
)

// TestAggregateDailyToMonthlyDeterministicOHLCV verifies that unordered daily
// input produces deterministic monthly output with correct OHLCV values and
// proper month ordering.
func TestAggregateDailyToMonthlyDeterministicOHLCV(t *testing.T) {
	// Create daily bars with unsorted timestamps.
	// Note: timestamps use UTC to avoid KST→UTC month boundary shift issues.
	// KST 2/1 00:00 → UTC 1/31 15:00 would fall into January group.
	input := []market.Bar{
		{
			InstrumentID: "005930:KRX",
			Timeframe:    market.TimeframeDaily,
			Timestamp:    time.Date(2024, 1, 3, 0, 0, 0, 0, time.UTC),
			Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67100"}},
			High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67800"}},
			Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66900"}},
			Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67500"}},
			Volume:       market.Quantity{Amount: market.Decimal{Value: "22000000"}},
		},
		{
			InstrumentID: "005930:KRX",
			Timeframe:    market.TimeframeDaily,
			Timestamp:    time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
			Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66700"}},
			High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67200"}},
			Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66400"}},
			Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67000"}},
			Volume:       market.Quantity{Amount: market.Decimal{Value: "25000000"}},
		},
		{
			InstrumentID: "005930:KRX",
			Timeframe:    market.TimeframeDaily,
			Timestamp:    time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC),
			Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "68000"}},
			High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "69500"}},
			Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67800"}},
			Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "69200"}},
			Volume:       market.Quantity{Amount: market.Decimal{Value: "30000000"}},
		},
		{
			InstrumentID: "005930:KRX",
			Timeframe:    market.TimeframeDaily,
			Timestamp:    time.Date(2024, 2, 2, 0, 0, 0, 0, time.UTC),
			Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "69300"}},
			High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "70000"}},
			Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "68800"}},
			Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "69800"}},
			Volume:       market.Quantity{Amount: market.Decimal{Value: "28000000"}},
		},
	}

	result, err := AggregateDailyToMonthly(input)
	if err != nil {
		t.Fatalf("AggregateDailyToMonthly unexpected error: %v", err)
	}

	// Verify we got 2 monthly bars.
	if len(result.Bars) != 2 {
		t.Fatalf("expected 2 monthly bars, got %d", len(result.Bars))
	}

	// Verify January bar.
	jan := result.Bars[0]
	if jan.Timeframe != market.TimeframeMonthly {
		t.Errorf("expected monthly timeframe, got %q", jan.Timeframe)
	}
	if jan.Timestamp.UTC() != time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) {
		t.Errorf("expected January 2024 timestamp, got %v", jan.Timestamp)
	}
	// ratToDecimal produces "66700.00000000" format.
	if jan.Open.Amount.Value != "66700.00000000" {
		t.Errorf("expected open 66700.00000000, got %s", jan.Open.Amount.Value)
	}
	if jan.Close.Amount.Value != "67500.00000000" {
		t.Errorf("expected close 67500.00000000, got %s", jan.Close.Amount.Value)
	}
	if jan.High.Amount.Value != "67800.00000000" {
		t.Errorf("expected high 67800.00000000, got %s", jan.High.Amount.Value)
	}
	if jan.Low.Amount.Value != "66400.00000000" {
		t.Errorf("expected low 66400.00000000, got %s", jan.Low.Amount.Value)
	}
	// Volume: 22000000 + 25000000 = 47000000
	if jan.Volume.Amount.Value != "47000000.00000000" {
		t.Errorf("expected volume 47000000.00000000 for Jan, got %s", jan.Volume.Amount.Value)
	}

	// Verify February bar.
	feb := result.Bars[1]
	if feb.Timestamp.UTC() != time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC) {
		t.Errorf("expected February 2024 timestamp, got %v", feb.Timestamp)
	}
	if feb.Open.Amount.Value != "68000.00000000" {
		t.Errorf("expected open 68000.00000000, got %s", feb.Open.Amount.Value)
	}
	if feb.Close.Amount.Value != "69800.00000000" {
		t.Errorf("expected close 69800.00000000, got %s", feb.Close.Amount.Value)
	}
	if feb.High.Amount.Value != "70000.00000000" {
		t.Errorf("expected high 70000.00000000, got %s", feb.High.Amount.Value)
	}
	if feb.Low.Amount.Value != "67800.00000000" {
		t.Errorf("expected low 67800.00000000, got %s", feb.Low.Amount.Value)
	}
	// Volume: 30000000 + 28000000 = 58000000
	if feb.Volume.Amount.Value != "58000000.00000000" {
		t.Errorf("expected volume 58000000.00000000 for Feb, got %s", feb.Volume.Amount.Value)
	}

	// Verify provenance — one per monthly bar, each with per-month source range/count.
	if len(result.Provenance) != 2 {
		t.Fatalf("expected 2 provenance records (one per monthly bar), got %d", len(result.Provenance))
	}

	// January provenance: source bars are Jan 2 and Jan 3 (UTC).
	janProv := result.Provenance[0]
	if janProv.SourceTimeframe != market.TimeframeDaily {
		t.Errorf("provenance[0]: expected source daily, got %q", janProv.SourceTimeframe)
	}
	if janProv.TargetTimeframe != market.TimeframeMonthly {
		t.Errorf("provenance[0]: expected target monthly, got %q", janProv.TargetTimeframe)
	}
	if janProv.SourceCount != 2 {
		t.Errorf("provenance[0]: expected source count 2 (Jan bars), got %d", janProv.SourceCount)
	}
	if janProv.SourceStart.UTC() != time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC) {
		t.Errorf("provenance[0]: expected source start 2024-01-02 UTC, got %v", janProv.SourceStart)
	}
	if janProv.SourceEnd.UTC() != time.Date(2024, 1, 3, 0, 0, 0, 0, time.UTC) {
		t.Errorf("provenance[0]: expected source end 2024-01-03 UTC, got %v", janProv.SourceEnd)
	}
	if janProv.AggregationRuleID != "monthly-deterministic-v1" {
		t.Errorf("provenance[0]: expected rule monthly-deterministic-v1, got %q", janProv.AggregationRuleID)
	}

	// February provenance: source bars are Feb 1 and Feb 2 (UTC).
	febProv := result.Provenance[1]
	if febProv.SourceTimeframe != market.TimeframeDaily {
		t.Errorf("provenance[1]: expected source daily, got %q", febProv.SourceTimeframe)
	}
	if febProv.TargetTimeframe != market.TimeframeMonthly {
		t.Errorf("provenance[1]: expected target monthly, got %q", febProv.TargetTimeframe)
	}
	if febProv.SourceCount != 2 {
		t.Errorf("provenance[1]: expected source count 2 (Feb bars), got %d", febProv.SourceCount)
	}
	if febProv.SourceStart.UTC() != time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC) {
		t.Errorf("provenance[1]: expected source start 2024-02-01 UTC, got %v", febProv.SourceStart)
	}
	if febProv.SourceEnd.UTC() != time.Date(2024, 2, 2, 0, 0, 0, 0, time.UTC) {
		t.Errorf("provenance[1]: expected source end 2024-02-02 UTC, got %v", febProv.SourceEnd)
	}
	if febProv.AggregationRuleID != "monthly-deterministic-v1" {
		t.Errorf("provenance[1]: expected rule monthly-deterministic-v1, got %q", febProv.AggregationRuleID)
	}
}

// TestAggregateDailyToMonthlyRejectsMixedInputs verifies that mixed instrument,
// non-daily timeframe, mixed currency, and invalid decimal inputs are rejected.
func TestAggregateDailyToMonthlyRejectsMixedInputs(t *testing.T) {
	tests := []struct {
		name  string
		input []market.Bar
	}{
		{
			name: "mixed instrument IDs",
			input: []market.Bar{
				{
					InstrumentID: "005930:KRX",
					Timeframe:    market.TimeframeDaily,
					Timestamp:    time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
					Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66700"}},
					High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67200"}},
					Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66400"}},
					Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67000"}},
					Volume:       market.Quantity{Amount: market.Decimal{Value: "25000000"}},
				},
				{
					InstrumentID: "000660:KRX",
					Timeframe:    market.TimeframeDaily,
					Timestamp:    time.Date(2024, 1, 3, 0, 0, 0, 0, time.UTC),
					Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67100"}},
					High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67800"}},
					Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66900"}},
					Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67500"}},
					Volume:       market.Quantity{Amount: market.Decimal{Value: "22000000"}},
				},
			},
		},
		{
			name: "mixed timeframe",
			input: []market.Bar{
				{
					InstrumentID: "005930:KRX",
					Timeframe:    market.TimeframeDaily,
					Timestamp:    time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
					Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66700"}},
					High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67200"}},
					Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66400"}},
					Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67000"}},
					Volume:       market.Quantity{Amount: market.Decimal{Value: "25000000"}},
				},
				{
					InstrumentID: "005930:KRX",
					Timeframe:    market.TimeframeMin5,
					Timestamp:    time.Date(2024, 1, 3, 0, 0, 0, 0, time.UTC),
					Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67100"}},
					High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67800"}},
					Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66900"}},
					Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67500"}},
					Volume:       market.Quantity{Amount: market.Decimal{Value: "22000000"}},
				},
			},
		},
		{
			name: "mixed currency",
			input: []market.Bar{
				{
					InstrumentID: "005930:KRX",
					Timeframe:    market.TimeframeDaily,
					Timestamp:    time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
					Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66700"}},
					High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67200"}},
					Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66400"}},
					Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67000"}},
					Volume:       market.Quantity{Amount: market.Decimal{Value: "25000000"}},
				},
				{
					InstrumentID: "005930:KRX",
					Timeframe:    market.TimeframeDaily,
					Timestamp:    time.Date(2024, 1, 3, 0, 0, 0, 0, time.UTC),
					Open:         market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "150.00"}},
					High:         market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "155.00"}},
					Low:          market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "148.00"}},
					Close:        market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "152.00"}},
					Volume:       market.Quantity{Amount: market.Decimal{Value: "1000000"}},
				},
			},
		},
		{
			name: "invalid zero price",
			input: []market.Bar{
				{
					InstrumentID: "005930:KRX",
					Timeframe:    market.TimeframeDaily,
					Timestamp:    time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
					Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "0"}},
					High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67200"}},
					Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66400"}},
					Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67000"}},
					Volume:       market.Quantity{Amount: market.Decimal{Value: "25000000"}},
				},
			},
		},
		{
			name: "invalid empty price",
			input: []market.Bar{
				{
					InstrumentID: "005930:KRX",
					Timeframe:    market.TimeframeDaily,
					Timestamp:    time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
					Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: ""}},
					High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67200"}},
					Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66400"}},
					Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67000"}},
					Volume:       market.Quantity{Amount: market.Decimal{Value: "25000000"}},
				},
			},
		},
		{
			name:  "empty input",
			input: []market.Bar{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := AggregateDailyToMonthly(tt.input)
			if err == nil {
				t.Errorf("expected error for %q, got nil", tt.name)
			}
		})
	}
}

// monthlyExpectedFixture represents the typed structure of monthly_expected.json.
type monthlyExpectedFixture struct {
	Bars       []expectedBar        `json:"bars"`
	Provenance []ExpectedProvenance `json:"provenance"`
}

// expectedBar is the expected bar from fixture.
type expectedBar struct {
	InstrumentID string           `json:"instrument_id"`
	Timeframe    market.Timeframe `json:"timeframe"`
	Timestamp    time.Time        `json:"timestamp"`
	Open         expectedPrice    `json:"open"`
	High         expectedPrice    `json:"high"`
	Low          expectedPrice    `json:"low"`
	Close        expectedPrice    `json:"close"`
	Volume       expectedVol      `json:"volume"`
}

// expectedPrice is the expected price from fixture.
type expectedPrice struct {
	Currency string `json:"currency"`
	Amount   string `json:"amount"`
}

// expectedVol is the expected volume from fixture.
type expectedVol struct {
	Amount string `json:"amount"`
}

// ExpectedProvenance represents the expected provenance from fixture.
type ExpectedProvenance struct {
	SourceTimeframe   market.Timeframe `json:"source_timeframe"`
	TargetTimeframe   market.Timeframe `json:"target_timeframe"`
	InstrumentID      string           `json:"instrument_id"`
	SourceStart       time.Time        `json:"source_start"`
	SourceEnd         time.Time        `json:"source_end"`
	SourceCount       int              `json:"source_count"`
	AggregationRuleID string           `json:"aggregation_rule_id"`
}

// TestAggregateDailyToMonthlyFixtureComparison verifies aggregation output
// against expected fixture files including OHLCV, currency, and provenance.
func TestAggregateDailyToMonthlyFixtureComparison(t *testing.T) {
	// Read input fixture.
	inputData, err := os.ReadFile("testdata/monthly_daily_input.json")
	if err != nil {
		t.Fatalf("read input fixture: %v", err)
	}

	// Parse input bars.
	var inputRaw []map[string]interface{}
	if err := json.Unmarshal(inputData, &inputRaw); err != nil {
		t.Fatalf("parse input fixture: %v", err)
	}

	inputBars := make([]market.Bar, 0, len(inputRaw))
	for _, raw := range inputRaw {
		// Parse timestamp.
		tsStr := raw["timestamp"].(string)
		ts, err := time.Parse(time.RFC3339, tsStr)
		if err != nil {
			t.Fatalf("parse timestamp %q: %v", tsStr, err)
		}

		openMap := raw["open"].(map[string]interface{})
		highMap := raw["high"].(map[string]interface{})
		lowMap := raw["low"].(map[string]interface{})
		closeMap := raw["close"].(map[string]interface{})
		volMap := raw["volume"].(map[string]interface{})

		openCurrency := market.Currency(openMap["currency"].(string))
		closeCurrency := market.Currency(closeMap["currency"].(string))

		bar := market.Bar{
			InstrumentID: market.InstrumentID(raw["instrument_id"].(string)),
			Timeframe:    market.Timeframe(raw["timeframe"].(string)),
			Timestamp:    ts,
			Open:         market.Price{Currency: openCurrency, Amount: market.Decimal{Value: openMap["amount"].(string)}},
			High:         market.Price{Currency: openCurrency, Amount: market.Decimal{Value: highMap["amount"].(string)}},
			Low:          market.Price{Currency: openCurrency, Amount: market.Decimal{Value: lowMap["amount"].(string)}},
			Close:        market.Price{Currency: closeCurrency, Amount: market.Decimal{Value: closeMap["amount"].(string)}},
			Volume:       market.Quantity{Amount: market.Decimal{Value: volMap["amount"].(string)}},
		}
		inputBars = append(inputBars, bar)
	}

	result, err := AggregateDailyToMonthly(inputBars)
	if err != nil {
		t.Fatalf("AggregateDailyToMonthly unexpected error: %v", err)
	}

	// Read expected fixture.
	expectedData, err := os.ReadFile("testdata/monthly_expected.json")
	if err != nil {
		t.Fatalf("read expected fixture: %v", err)
	}

	// Parse expected fixture: the file contains a single-element array wrapping MonthlyResult.
	var expectedArr []monthlyExpectedFixture
	if err := json.Unmarshal(expectedData, &expectedArr); err != nil {
		t.Fatalf("parse expected fixture: %v", err)
	}
	if len(expectedArr) == 0 {
		t.Fatalf("expected fixture array is empty")
	}
	expected := expectedArr[0]

	// Compare bars count.
	if len(result.Bars) != len(expected.Bars) {
		t.Errorf("expected %d bars, got %d", len(expected.Bars), len(result.Bars))
	}

	// Compare each bar including OHLCV, currency, timestamp.
	for i, exp := range expected.Bars {
		if i >= len(result.Bars) {
			t.Errorf("bar %d: expected bar exists but got none", i)
			continue
		}

		got := result.Bars[i]

		// Compare instrument_id.
		if string(got.InstrumentID) != exp.InstrumentID {
			t.Errorf("bar %d: instrument_id = %q, want %q", i, got.InstrumentID, exp.InstrumentID)
		}

		// Compare timeframe.
		if got.Timeframe != market.TimeframeMonthly {
			t.Errorf("bar %d: timeframe = %q, want %q", i, got.Timeframe, "1mo")
		}

		// Compare timestamp.
		if !got.Timestamp.Equal(exp.Timestamp) {
			t.Errorf("bar %d: timestamp = %v, want %v", i, got.Timestamp, exp.Timestamp)
		}

		// Compare OHLCV amounts and currency.
		if string(got.Open.Currency) != exp.Open.Currency {
			t.Errorf("bar %d: open currency = %q, want %q", i, got.Open.Currency, exp.Open.Currency)
		}
		if got.Open.Amount.Value != exp.Open.Amount {
			t.Errorf("bar %d: open amount = %q, want %q", i, got.Open.Amount.Value, exp.Open.Amount)
		}
		if string(got.High.Currency) != exp.High.Currency {
			t.Errorf("bar %d: high currency = %q, want %q", i, got.High.Currency, exp.High.Currency)
		}
		if got.High.Amount.Value != exp.High.Amount {
			t.Errorf("bar %d: high amount = %q, want %q", i, got.High.Amount.Value, exp.High.Amount)
		}
		if string(got.Low.Currency) != exp.Low.Currency {
			t.Errorf("bar %d: low currency = %q, want %q", i, got.Low.Currency, exp.Low.Currency)
		}
		if got.Low.Amount.Value != exp.Low.Amount {
			t.Errorf("bar %d: low amount = %q, want %q", i, got.Low.Amount.Value, exp.Low.Amount)
		}
		if string(got.Close.Currency) != exp.Close.Currency {
			t.Errorf("bar %d: close currency = %q, want %q", i, got.Close.Currency, exp.Close.Currency)
		}
		if got.Close.Amount.Value != exp.Close.Amount {
			t.Errorf("bar %d: close amount = %q, want %q", i, got.Close.Amount.Value, exp.Close.Amount)
		}
		if got.Volume.Amount.Value != exp.Volume.Amount {
			t.Errorf("bar %d: volume amount = %q, want %q", i, got.Volume.Amount.Value, exp.Volume.Amount)
		}
	}

	// Compare provenance count.
	if len(result.Provenance) != len(expected.Provenance) {
		t.Errorf("expected %d provenance records, got %d", len(expected.Provenance), len(result.Provenance))
	}

	// Compare each provenance record.
	for i, exp := range expected.Provenance {
		if i >= len(result.Provenance) {
			t.Errorf("provenance %d: expected record exists but got none", i)
			continue
		}

		got := result.Provenance[i]

		// Compare source timeframe.
		if got.SourceTimeframe != exp.SourceTimeframe {
			t.Errorf("provenance %d: source_timeframe = %q, want %q", i, got.SourceTimeframe, exp.SourceTimeframe)
		}

		// Compare target timeframe.
		if got.TargetTimeframe != exp.TargetTimeframe {
			t.Errorf("provenance %d: target_timeframe = %q, want %q", i, got.TargetTimeframe, exp.TargetTimeframe)
		}

		// Compare instrument_id.
		if string(got.InstrumentID) != exp.InstrumentID {
			t.Errorf("provenance %d: instrument_id = %q, want %q", i, got.InstrumentID, exp.InstrumentID)
		}

		// Compare source_start.
		if !got.SourceStart.Equal(exp.SourceStart) {
			t.Errorf("provenance %d: source_start = %v, want %v", i, got.SourceStart, exp.SourceStart)
		}

		// Compare source_end.
		if !got.SourceEnd.Equal(exp.SourceEnd) {
			t.Errorf("provenance %d: source_end = %v, want %v", i, got.SourceEnd, exp.SourceEnd)
		}

		// Compare source_count.
		if got.SourceCount != exp.SourceCount {
			t.Errorf("provenance %d: source_count = %d, want %d", i, got.SourceCount, exp.SourceCount)
		}

		// Compare aggregation_rule_id.
		if got.AggregationRuleID != exp.AggregationRuleID {
			t.Errorf("provenance %d: aggregation_rule_id = %q, want %q", i, got.AggregationRuleID, exp.AggregationRuleID)
		}
	}
}
