"""Tests for Lab 01 — the LLM gateway and model abstraction layer.

    pytest test_lab.py -v
    LAB_MODULE=solution pytest test_lab.py -v    # the reference — must be green
"""

import importlib
import os

import pytest

lab = importlib.import_module(os.environ.get("LAB_MODULE", "lab"))


def clock(step=0.05):
    state = {"t": -step}

    def now():
        state["t"] += step
        return round(state["t"], 6)

    return now


def frozen(value=0.0):
    return lambda: value


# ======================================================================================
# 1. The normalized request
# ======================================================================================


def msgs(user="hello"):
    return (lab.Message("system", "be helpful"), lab.Message("user", user))


def req(user=None, **kwargs):
    kwargs.setdefault("messages", msgs(user) if user is not None else msgs())
    kwargs.setdefault("task_class", lab.TaskClass.CHAT)
    kwargs.setdefault("tenant", "wholesale")
    kwargs.setdefault("agent_id", "a1")
    return lab.NormalizedRequest(**kwargs)


def test_request_validates_itself():
    with pytest.raises(ValueError):
        req(messages=())
    with pytest.raises(ValueError):
        req(max_output_tokens=0)
    with pytest.raises(ValueError):
        req(temperature=3.0)
    with pytest.raises(ValueError):
        req(latency_budget_ms=0)


def test_prompt_text_and_stable_prefix():
    r = req(user="why is PMT-771 held?")
    assert "system: be helpful" in r.prompt_text()
    assert "user: why is PMT-771 held?" in r.prompt_text()
    assert r.stable_prefix() == "system: be helpful"


def test_usage_validates():
    with pytest.raises(ValueError):
        lab.Usage(input_tokens=-1)
    with pytest.raises(ValueError):
        lab.Usage(input_tokens=5, cached_input_tokens=6)
    assert lab.Usage(input_tokens=10, output_tokens=5).total == 15


# ======================================================================================
# 2. Error taxonomy
# ======================================================================================


@pytest.mark.parametrize("cls,retryable,fall_over", [
    (lab.RateLimited, True, True),
    (lab.ProviderTimeout, True, True),
    (lab.ProviderUnavailable, True, True),
    (lab.ContentFiltered, False, False),
    (lab.InvalidRequest, False, False),
    (lab.QuotaExceeded, False, False),
    (lab.NoRouteAvailable, False, False),
    (lab.BudgetExhausted, False, False),
])
def test_error_taxonomy(cls, retryable, fall_over):
    exc = cls("boom")
    assert exc.retryable is retryable
    assert exc.fall_over is fall_over


def test_content_filter_is_never_a_reason_to_shop_for_another_model():
    assert lab.ContentFiltered("x").fall_over is False


# ======================================================================================
# 3. Deployments and cost
# ======================================================================================


def deployment(name="d", **kwargs):
    kwargs.setdefault("provider", "azure")
    kwargs.setdefault("model", "m")
    kwargs.setdefault("region", "uae-north")
    kwargs.setdefault("capacity", lab.Capacity.PAYG)
    kwargs.setdefault("input_micros_per_1k", 3000)
    kwargs.setdefault("cached_input_micros_per_1k", 300)
    kwargs.setdefault("output_micros_per_1k", 15000)
    kwargs.setdefault("expected_latency_ms", 800)
    return lab.Deployment(name=name, **kwargs)


def test_cost_uses_three_price_tiers():
    d = deployment()
    usage = lab.Usage(input_tokens=2000, cached_input_tokens=1000, output_tokens=500)
    # (1000*3000 + 1000*300 + 500*15000) / 1000
    assert d.cost_micros(usage) == 10800


def test_cached_tokens_are_cheaper():
    d = deployment()
    plain = d.cost_micros(lab.Usage(input_tokens=1000, output_tokens=0))
    cached = d.cost_micros(lab.Usage(input_tokens=1000, cached_input_tokens=1000, output_tokens=0))
    assert cached < plain


def test_classification_rank_rejects_unknown():
    with pytest.raises(ValueError):
        lab.classification_rank("cosmic")
    assert lab.classification_rank("public") < lab.classification_rank("restricted")


# ======================================================================================
# 4. Routing
# ======================================================================================


def routing_setup():
    deployments = {
        "ptu": deployment("ptu", capacity=lab.Capacity.PROVISIONED, expected_latency_ms=700),
        "payg": deployment("payg", expected_latency_ms=900),
        "eu": deployment("eu", provider="anthropic", region="eu-west",
                         max_classification="confidential", expected_latency_ms=800),
        "local": deployment("local", provider="vllm", capacity=lab.Capacity.SELF_HOSTED,
                            input_micros_per_1k=400, cached_input_micros_per_1k=400,
                            output_micros_per_1k=800, expected_latency_ms=1400),
    }
    rules = [
        lab.RoutingRule("restricted-onshore", ("ptu", "payg", "local"),
                        classifications=("restricted",), priority=10),
        lab.RoutingRule("cheap-classification", ("local", "payg"),
                        task_classes=(lab.TaskClass.CLASSIFICATION,), priority=20),
        lab.RoutingRule("default", ("ptu", "eu", "payg"), priority=100),
    ]
    return lab.Router(deployments, rules)


def test_router_rejects_rules_naming_unknown_deployments():
    with pytest.raises(ValueError):
        lab.Router({"a": deployment("a")}, [lab.RoutingRule("r", ("a", "ghost"))])


def test_routing_rule_rejects_a_typo_in_a_classification():
    with pytest.raises(ValueError):
        lab.RoutingRule("r", ("a",), classifications=("resticted",))


def test_default_rule_applies_when_nothing_more_specific_matches():
    router = routing_setup()
    assert [d.name for d in router.candidates(req())] == ["ptu", "eu", "payg"]


def test_lower_priority_number_wins():
    router = routing_setup()
    chain = router.candidates(req(data_classification="restricted"))
    assert [d.name for d in chain] == ["ptu", "payg", "local"]


def test_task_class_routing():
    router = routing_setup()
    chain = router.candidates(req(task_class=lab.TaskClass.CLASSIFICATION))
    assert [d.name for d in chain] == ["local", "payg"]


def test_residency_removes_offshore_deployments():
    router = routing_setup()
    chain = router.candidates(req(residency="uae-north"))
    assert "eu" not in [d.name for d in chain]


def test_deployment_classification_ceiling_is_a_second_gate():
    """The rule matched; the deployment still refuses data above its ceiling."""
    router = routing_setup()
    chain = router.candidates(req(data_classification="restricted"))
    assert "eu" not in [d.name for d in chain]


def test_impossible_constraints_produce_no_route():
    router = routing_setup()
    assert router.candidates(req(data_classification="restricted", residency="eu-west")) == []


def test_tenant_scoped_rule():
    router = lab.Router(
        {"a": deployment("a"), "b": deployment("b")},
        [lab.RoutingRule("vip", ("a",), tenants=("wholesale",), priority=5),
         lab.RoutingRule("default", ("b",), priority=100)])
    assert [d.name for d in router.candidates(req(tenant="wholesale"))] == ["a"]
    assert [d.name for d in router.candidates(req(tenant="retail"))] == ["b"]


# ======================================================================================
# 5. Token bucket, rate limiting, quotas
# ======================================================================================


def test_token_bucket_bursts_then_throttles():
    bucket = lab.TokenBucket(10, 1.0, now=frozen(0.0))
    assert all(bucket.try_consume(1) for _ in range(10))
    assert not bucket.try_consume(1)


def test_token_bucket_never_goes_negative():
    bucket = lab.TokenBucket(10, 1.0, now=frozen(0.0))
    bucket.try_consume(10)
    bucket.try_consume(5)
    assert bucket.tokens >= 0


def test_token_bucket_refills_with_the_clock():
    t = {"v": 0.0}
    bucket = lab.TokenBucket(10, 2.0, now=lambda: t["v"])
    bucket.try_consume(10)
    assert not bucket.try_consume(1)
    t["v"] = 1.0                     # 2 tokens/second
    assert bucket.try_consume(2)


def test_token_bucket_caps_at_capacity():
    t = {"v": 0.0}
    bucket = lab.TokenBucket(10, 100.0, now=lambda: t["v"])
    bucket.try_consume(10)
    t["v"] = 1000.0
    bucket._refill()
    assert bucket.tokens == 10


def test_token_bucket_retry_after():
    bucket = lab.TokenBucket(10, 2.0, now=frozen(0.0))
    bucket.try_consume(10)
    assert bucket.retry_after_seconds(4) == pytest.approx(2.0)


def test_token_bucket_rejects_bad_config():
    with pytest.raises(ValueError):
        lab.TokenBucket(0, 1.0, now=frozen())
    with pytest.raises(ValueError):
        lab.TokenBucket(1, 0, now=frozen())


def limits():
    return {"wholesale": lab.TenantLimits(60, 120_000, 50_000_000),
            "retail": lab.TenantLimits(6, 6_000, 1_000_000)}


def test_rate_limiter_enforces_requests_per_minute():
    limiter = lab.RateLimiter(limits(), now=frozen(0.0))
    for _ in range(6):
        limiter.admit("retail", 1)
    with pytest.raises(lab.RateLimited):
        limiter.admit("retail", 1)


def test_rate_limiter_enforces_tokens_per_minute():
    limiter = lab.RateLimiter(limits(), now=frozen(0.0))
    limiter.admit("retail", 6_000)
    with pytest.raises(lab.RateLimited):
        limiter.admit("retail", 1)


def test_a_rejected_request_does_not_spend_the_request_budget():
    """Rejecting on tokens must not also consume an RPM slot."""
    limiter = lab.RateLimiter(limits(), now=frozen(0.0))
    with pytest.raises(lab.RateLimited):
        limiter.admit("retail", 10_000)          # over TPM immediately
    for _ in range(6):
        limiter.admit("retail", 1)               # all six RPM slots still available


def test_unknown_tenant_is_refused():
    limiter = lab.RateLimiter(limits(), now=frozen(0.0))
    with pytest.raises(lab.QuotaExceeded):
        limiter.admit("ghost", 1)


def test_quota_ledger_tracks_and_stops():
    ledger = lab.QuotaLedger(limits())
    ledger.check("retail")
    ledger.record("retail", 999_999)
    ledger.check("retail")
    assert ledger.remaining("retail") == 1
    ledger.record("retail", 1)
    with pytest.raises(lab.QuotaExceeded):
        ledger.check("retail")
    assert ledger.remaining("retail") == 0


def test_quota_boundary_is_inclusive():
    ledger = lab.QuotaLedger({"t": lab.TenantLimits(10, 10, 100)})
    ledger.record("t", 100)
    with pytest.raises(lab.QuotaExceeded):
        ledger.check("t")


# ======================================================================================
# 6. Caching
# ======================================================================================


def test_hash_embed_is_deterministic_and_normalized():
    a = lab.hash_embed("payment PMT-771 is held")
    b = lab.hash_embed("payment PMT-771 is held")
    assert a == b
    assert lab.cosine(a, a) == pytest.approx(1.0, abs=1e-9)


def test_hash_embed_of_empty_text():
    assert lab.cosine(lab.hash_embed(""), lab.hash_embed("")) == 0.0


def test_similar_text_scores_higher_than_unrelated():
    query = lab.hash_embed("why is payment PMT-771 held")
    near = lab.hash_embed("why is payment PMT-771 held today")
    far = lab.hash_embed("draft a marketing email about savings accounts")
    assert lab.cosine(query, near) > lab.cosine(query, far)


def test_cache_key_starts_with_the_tenant():
    a = lab.cache_key(req(tenant="wholesale"), "d")
    b = lab.cache_key(req(tenant="retail"), "d")
    assert a != b


def test_cache_key_varies_with_the_parameters_that_change_the_answer():
    base = req()
    assert lab.cache_key(base, "d") != lab.cache_key(req(temperature=0.7), "d")
    assert lab.cache_key(base, "d") != lab.cache_key(req(max_output_tokens=99), "d")
    assert lab.cache_key(base, "d") != lab.cache_key(base, "other-deployment")


def response(text="hi", **kwargs):
    kwargs.setdefault("finish_reason", lab.FinishReason.STOP)
    kwargs.setdefault("usage", lab.Usage(10, 0, 5))
    kwargs.setdefault("deployment", "d")
    kwargs.setdefault("provider", "azure")
    kwargs.setdefault("model", "m")
    kwargs.setdefault("latency_ms", 100)
    return lab.NormalizedResponse(text=text, **kwargs)


def test_exact_cache_hit_and_expiry():
    t = {"v": 0.0}
    cache = lab.ExactCache(ttl_seconds=10, now=lambda: t["v"])
    cache.put("k", response())
    assert cache.get("k") is not None
    t["v"] = 11.0
    assert cache.get("k") is None
    assert cache.hits == 1 and cache.misses == 1


def test_exact_cache_evicts_at_capacity():
    t = {"v": 0.0}
    cache = lab.ExactCache(ttl_seconds=100, now=lambda: t["v"], capacity=2)
    for i in range(3):
        t["v"] = float(i)
        cache.put(f"k{i}", response())
    assert cache.get("k0") is None
    assert cache.get("k2") is not None


def test_semantic_cache_is_tenant_partitioned():
    cache = lab.SemanticCache(threshold=0.5, ttl_seconds=100, now=frozen(0.0))
    cache.put(req(tenant="wholesale", user="why is PMT-771 held"), response("wholesale answer"))
    assert cache.get(req(tenant="wholesale", user="why is PMT-771 held")) is not None
    assert cache.get(req(tenant="retail", user="why is PMT-771 held")) is None


def test_semantic_cache_respects_the_similarity_floor():
    cache = lab.SemanticCache(threshold=0.99, ttl_seconds=100, now=frozen(0.0))
    cache.put(req(user="why is payment PMT-771 held"), response())
    assert cache.get(req(user="draft a marketing email")) is None


def test_semantic_cache_expires():
    t = {"v": 0.0}
    cache = lab.SemanticCache(threshold=0.5, ttl_seconds=10, now=lambda: t["v"])
    cache.put(req(user="a question"), response())
    t["v"] = 11.0
    assert cache.get(req(user="a question")) is None


def test_semantic_cache_rejects_a_bad_threshold():
    with pytest.raises(ValueError):
        lab.SemanticCache(threshold=0.0, ttl_seconds=1, now=frozen())
    with pytest.raises(ValueError):
        lab.SemanticCache(threshold=1.5, ttl_seconds=1, now=frozen())


# ======================================================================================
# 7. The gateway, end to end
# ======================================================================================


def build(*, adapters=None, now=None, exact=True, semantic=True, threshold=0.92):
    now = now or clock()
    router = routing_setup()
    lim = limits()
    accounting = lab.Accounting()
    gateway = lab.Gateway(
        router=router,
        adapters=adapters or {
            "azure": lab.make_scripted_adapter("azure", reply=lambda r: "azure answer"),
            "anthropic": lab.make_scripted_adapter("anthropic", reply=lambda r: "anthropic answer"),
            "vllm": lab.make_scripted_adapter("vllm", reply=lambda r: "local answer"),
        },
        rate_limiter=lab.RateLimiter(lim, now=now),
        quotas=lab.QuotaLedger(lim),
        accounting=accounting,
        now=now,
        exact_cache=lab.ExactCache(ttl_seconds=300, now=now) if exact else None,
        semantic_cache=(lab.SemanticCache(threshold=threshold, ttl_seconds=300, now=now)
                        if semantic else None),
    )
    return gateway, accounting


def test_happy_path_routes_accounts_and_returns():
    gateway, accounting = build()
    r = gateway.complete(req())
    assert r.deployment == "ptu"
    assert r.provider == "azure"
    assert r.cost_micros > 0
    assert r.attempts == ("ptu",)
    assert len(accounting.records) == 1
    assert accounting.records[0].outcome == "ok"


def test_no_route_is_refused_and_accounted():
    gateway, accounting = build()
    with pytest.raises(lab.NoRouteAvailable):
        gateway.complete(req(data_classification="restricted", residency="eu-west"))
    assert accounting.records[0].outcome == "NoRouteAvailable"


def test_fallback_on_a_retryable_error():
    gateway, _ = build(adapters={
        "azure": lab.make_scripted_adapter("azure", reply=lambda r: "ok",
                                           failures=[lab.RateLimited("429")]),
        "anthropic": lab.make_scripted_adapter("anthropic", reply=lambda r: "anthropic answer"),
        "vllm": lab.make_scripted_adapter("vllm", reply=lambda r: "local"),
    })
    r = gateway.complete(req())
    assert r.deployment == "eu"
    assert r.attempts == ("ptu", "eu")


def test_no_fallback_on_a_content_filter():
    gateway, accounting = build(adapters={
        "azure": lab.make_scripted_adapter("azure", reply=lambda r: "x",
                                           failures=[lab.ContentFiltered("blocked")]),
        "anthropic": lab.make_scripted_adapter("anthropic", reply=lambda r: "anthropic"),
        "vllm": lab.make_scripted_adapter("vllm", reply=lambda r: "local"),
    })
    with pytest.raises(lab.ContentFiltered):
        gateway.complete(req())
    assert accounting.records[0].attempts == ("ptu",)


def test_no_fallback_for_a_side_effecting_request():
    gateway, _ = build(adapters={
        "azure": lab.make_scripted_adapter("azure", reply=lambda r: "ok",
                                           failures=[lab.ProviderTimeout("timeout")]),
        "anthropic": lab.make_scripted_adapter("anthropic", reply=lambda r: "anthropic"),
        "vllm": lab.make_scripted_adapter("vllm", reply=lambda r: "local"),
    })
    with pytest.raises(lab.ProviderTimeout):
        gateway.complete(req(side_effecting=True))


def test_fallback_refused_when_it_does_not_fit_the_budget():
    gateway, _ = build(now=clock(step=0.6), adapters={
        "azure": lab.make_scripted_adapter("azure", reply=lambda r: "ok",
                                           failures=[lab.ProviderTimeout("timeout")]),
        "anthropic": lab.make_scripted_adapter("anthropic", reply=lambda r: "anthropic"),
        "vllm": lab.make_scripted_adapter("vllm", reply=lambda r: "local"),
    })
    with pytest.raises(lab.BudgetExhausted):
        gateway.complete(req(latency_budget_ms=1000))


def test_fallback_allowed_when_the_budget_is_generous():
    gateway, _ = build(adapters={
        "azure": lab.make_scripted_adapter("azure", reply=lambda r: "ok",
                                           failures=[lab.ProviderTimeout("timeout")]),
        "anthropic": lab.make_scripted_adapter("anthropic", reply=lambda r: "anthropic"),
        "vllm": lab.make_scripted_adapter("vllm", reply=lambda r: "local"),
    })
    assert gateway.complete(req(latency_budget_ms=30_000)).deployment == "eu"


def test_every_candidate_failing_raises_the_last_error():
    # azure serves both "ptu" and "payg", so it must fail twice for the chain to exhaust.
    gateway, _ = build(adapters={
        "azure": lab.make_scripted_adapter(
            "azure", reply=lambda r: "x",
            failures=[lab.RateLimited("429"), lab.ProviderUnavailable("503")]),
        "anthropic": lab.make_scripted_adapter("anthropic", reply=lambda r: "x",
                                               failures=[lab.ProviderUnavailable("503")]),
        "vllm": lab.make_scripted_adapter("vllm", reply=lambda r: "x",
                                          failures=[lab.ProviderUnavailable("503")]),
    })
    with pytest.raises(lab.ProviderUnavailable):
        gateway.complete(req(latency_budget_ms=30_000))


def test_quota_is_checked_before_the_provider_is_called():
    calls = []
    gateway, _ = build(adapters={
        "azure": lambda r, d: calls.append(1) or response(),
        "anthropic": lambda r, d: response(),
        "vllm": lambda r, d: response(),
    })
    gateway.quotas.record("wholesale", 50_000_000)
    with pytest.raises(lab.QuotaExceeded):
        gateway.complete(req())
    assert calls == []


def test_rate_limit_is_checked_before_the_provider_is_called():
    calls = []
    gateway, _ = build(now=frozen(0.0), adapters={
        "azure": lambda r, d: calls.append(1) or response(),
        "anthropic": lambda r, d: response(),
        "vllm": lambda r, d: response(),
    })
    for _ in range(6):
        gateway.complete(req(tenant="retail", cacheable=False, user=f"q{len(calls)}"))
    with pytest.raises(lab.RateLimited):
        gateway.complete(req(tenant="retail", cacheable=False, user="one more"))
    assert len(calls) == 6


def test_exact_cache_hit_costs_nothing_and_calls_no_provider():
    calls = []

    def counting(r, d):
        calls.append(1)
        return response()

    gateway, accounting = build(adapters={"azure": counting,
                                          "anthropic": counting, "vllm": counting})
    gateway.complete(req())
    hit = gateway.complete(req())
    assert hit.cache == "exact"
    assert hit.cost_micros == 0
    assert len(calls) == 1
    assert accounting.records[-1].cost_micros == 0


def test_semantic_cache_catches_a_near_duplicate():
    gateway, _ = build(threshold=0.8)
    gateway.complete(req(user="why is payment PMT-771 held"))
    hit = gateway.complete(req(user="why is payment PMT-771 held today"))
    assert hit.cache == "semantic"


def test_a_cache_never_crosses_a_tenant_boundary():
    gateway, _ = build(threshold=0.5)
    gateway.complete(req(tenant="wholesale", user="why is PMT-771 held"))
    other = gateway.complete(req(tenant="retail", user="why is PMT-771 held"))
    assert other.cache == "miss"


def test_uncacheable_requests_are_never_stored_or_served():
    gateway, _ = build()
    gateway.complete(req(cacheable=False))
    again = gateway.complete(req(cacheable=False))
    assert again.cache == "miss"


def test_a_truncated_answer_is_never_cached():
    gateway, _ = build(adapters={
        "azure": lab.make_scripted_adapter("azure", reply=lambda r: "cut off",
                                           finish_reason=lab.FinishReason.LENGTH),
        "anthropic": lab.make_scripted_adapter("anthropic", reply=lambda r: "x"),
        "vllm": lab.make_scripted_adapter("vllm", reply=lambda r: "x"),
    })
    gateway.complete(req())
    assert gateway.complete(req()).cache == "miss"


# ======================================================================================
# 8. Accounting
# ======================================================================================


def test_cost_attribution_by_dimension():
    gateway, accounting = build()
    for tenant, agent in [("wholesale", "a1"), ("wholesale", "a2"), ("retail", "a3")]:
        gateway.complete(req(tenant=tenant, agent_id=agent, cacheable=False,
                             user=f"{agent} asks"))
    by_tenant = accounting.cost_by("tenant")
    assert set(by_tenant) == {"wholesale", "retail"}
    assert by_tenant["wholesale"] > by_tenant["retail"]
    assert set(accounting.cost_by("agent_id")) == {"a1", "a2", "a3"}
    assert set(accounting.cost_by("provider")) == {"azure"}


def test_attribution_by_an_unknown_dimension_is_refused():
    _, accounting = build()
    with pytest.raises(ValueError):
        accounting.cost_by("colour")


def test_failures_are_accounted_with_zero_cost():
    gateway, accounting = build(adapters={
        "azure": lab.make_scripted_adapter("azure", reply=lambda r: "x",
                                           failures=[lab.ContentFiltered("blocked")]),
        "anthropic": lab.make_scripted_adapter("anthropic", reply=lambda r: "x"),
        "vllm": lab.make_scripted_adapter("vllm", reply=lambda r: "x"),
    })
    with pytest.raises(lab.ContentFiltered):
        gateway.complete(req())
    assert len(accounting.records) == 1
    assert accounting.records[0].outcome == "ContentFiltered"
    assert accounting.records[0].cost_micros == 0


def test_cache_hit_rate_and_failover_rate():
    gateway, accounting = build()
    gateway.complete(req())
    gateway.complete(req())
    assert accounting.cache_hit_rate() == pytest.approx(0.5)
    assert accounting.failover_rate() == 0.0


def test_failover_rate_detects_a_degrading_provider():
    gateway, accounting = build(adapters={
        "azure": lab.make_scripted_adapter("azure", reply=lambda r: "x",
                                           failures=[lab.RateLimited("429")]),
        "anthropic": lab.make_scripted_adapter("anthropic", reply=lambda r: "anthropic"),
        "vllm": lab.make_scripted_adapter("vllm", reply=lambda r: "x"),
    })
    gateway.complete(req())
    assert accounting.failover_rate() == pytest.approx(1.0)


def test_tokens_by_tenant():
    gateway, accounting = build()
    gateway.complete(req(tenant="wholesale", cacheable=False, user="a"))
    gateway.complete(req(tenant="retail", cacheable=False, user="b"))
    assert set(accounting.tokens_by_tenant()) == {"wholesale", "retail"}


# ======================================================================================
# 9. Determinism
# ======================================================================================


def test_two_identical_gateways_agree():
    a, acc_a = build()
    b, acc_b = build()
    ra = a.complete(req())
    rb = b.complete(req())
    assert ra == rb
    assert acc_a.records == acc_b.records
