# ruff: noqa: INP001
"""Tests for provider usage scraper parsers.

All tests are pure-Python — no subprocess, no tmux, no gateway connection.
Each fixture string represents a realistic sample of `claude /usage` output.
Tests are written first and drive the parser implementation.
"""

from __future__ import annotations

import pytest

# We import the parse function directly — it is pure and has no side effects.
from app.services.openclaw.usage_scrapers import parse_claude_usage


# ---------------------------------------------------------------------------
# Fixture text samples
# ---------------------------------------------------------------------------

# Standard output from `claude /usage` in Claude Code (subscription plan)
FIXTURE_STANDARD = """
╭──────────────────────────────────────────────────────────────────╮
│                       Claude Code Usage                          │
╰──────────────────────────────────────────────────────────────────╯

 Usage window (resets in 2h 47m):
   67% of limit used

 This week (Mon – Sun):
   Messages:      234
   Input tokens:  1,234,567
   Output tokens:   456,789
   Est. cost:     $4.23
"""

# Minimal output — just the percentage and reset time
FIXTURE_MINIMAL = """
Rate limit: 45% used
Resets in: 3h 15m
"""

# Output with messages limit shown as X/Y
FIXTURE_WITH_LIMIT = """
Usage window resets in 1h 5m:
  Messages: 178 / 500  (35% used)
  Input tokens: 890,123
  Output tokens: 234,567

Weekly usage:
  Messages: 892
  Cost: $8.76
"""

# Sub-minute remaining time
FIXTURE_ALMOST_RESET = """
Usage: 99% of window used
Resets in: 42m
"""

# Only minutes remaining without hours
FIXTURE_MINUTES_ONLY = """
Context: 72% used
Time remaining: 28m
"""

# Days + hours format
FIXTURE_DAYS_HOURS = """
Next reset: in 1 day 4h
Current usage: 12%
"""

# "< 1 minute" edge case
FIXTURE_UNDER_ONE_MINUTE = """
Usage: 100%
Resets in: < 1m
"""

# Output without any percentage but with reset time
FIXTURE_NO_PCT = """
Session active.
Window resets in: 4h 0m
No usage data available.
"""

# Output with zero usage
FIXTURE_ZERO_USAGE = """
Usage window (resets in 5h 0m):
  0% of limit used

Weekly: 0 messages, $0.00
"""

# Completely empty
FIXTURE_EMPTY = ""

# Garbage / error text
FIXTURE_ERROR = "Error: claude: command not found"

# Multi-line noise around the real data
FIXTURE_NOISY = """
...
Checking session...
Fetching usage stats...

Usage (resets in 2h 0m): 55% used

Messages this week: 301

...Done.
"""

# Alternate key casing variants that some versions might use
FIXTURE_ALT_KEYS = """
rate limit window: 89% Used
RESETS IN: 0h 30m
weekly messages: 412
weekly tokens: 9,876,543
"""


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestParseClaudeUsagePercentage:

    def test_standard_percentage(self):
        r = parse_claude_usage(FIXTURE_STANDARD)
        assert r.current_pct == pytest.approx(67.0)

    def test_minimal_percentage(self):
        r = parse_claude_usage(FIXTURE_MINIMAL)
        assert r.current_pct == pytest.approx(45.0)

    def test_with_limit_percentage(self):
        r = parse_claude_usage(FIXTURE_WITH_LIMIT)
        assert r.current_pct == pytest.approx(35.0)

    def test_near_full_percentage(self):
        r = parse_claude_usage(FIXTURE_ALMOST_RESET)
        assert r.current_pct == pytest.approx(99.0)

    def test_zero_percentage(self):
        r = parse_claude_usage(FIXTURE_ZERO_USAGE)
        assert r.current_pct == pytest.approx(0.0)

    def test_no_percentage_returns_none(self):
        r = parse_claude_usage(FIXTURE_NO_PCT)
        assert r.current_pct is None

    def test_empty_returns_none(self):
        r = parse_claude_usage(FIXTURE_EMPTY)
        assert r.current_pct is None

    def test_error_text_returns_none(self):
        r = parse_claude_usage(FIXTURE_ERROR)
        assert r.current_pct is None

    def test_alt_key_casing(self):
        r = parse_claude_usage(FIXTURE_ALT_KEYS)
        assert r.current_pct == pytest.approx(89.0)

    def test_noisy_output(self):
        r = parse_claude_usage(FIXTURE_NOISY)
        assert r.current_pct == pytest.approx(55.0)


class TestParseClaudeUsageRemainingTime:

    def test_hours_and_minutes(self):
        r = parse_claude_usage(FIXTURE_STANDARD)
        # 2h 47m = 10020 seconds = 10,020,000 ms
        assert r.remaining_ms == pytest.approx(10_020_000, rel=1e-3)
        assert r.remaining_label == "2h 47m"

    def test_hours_and_minutes_variant(self):
        r = parse_claude_usage(FIXTURE_MINIMAL)
        # 3h 15m = 11700 seconds
        assert r.remaining_ms == pytest.approx(11_700_000, rel=1e-3)

    def test_minutes_only(self):
        r = parse_claude_usage(FIXTURE_MINUTES_ONLY)
        # 28m = 1680 seconds
        assert r.remaining_ms == pytest.approx(1_680_000, rel=1e-3)
        assert r.remaining_label == "28m"

    def test_days_and_hours(self):
        r = parse_claude_usage(FIXTURE_DAYS_HOURS)
        # 1 day 4h = 28 hours = 100800 seconds
        assert r.remaining_ms == pytest.approx(100_800_000, rel=1e-3)

    def test_under_one_minute(self):
        r = parse_claude_usage(FIXTURE_UNDER_ONE_MINUTE)
        assert r.remaining_ms is not None
        assert r.remaining_ms < 60_000  # less than 1 minute

    def test_short_reset(self):
        r = parse_claude_usage(FIXTURE_ALMOST_RESET)
        # 42m = 2520 seconds
        assert r.remaining_ms == pytest.approx(2_520_000, rel=1e-3)

    def test_no_time_returns_none(self):
        r = parse_claude_usage(FIXTURE_ERROR)
        assert r.remaining_ms is None
        assert r.remaining_label is None

    def test_empty_returns_none(self):
        r = parse_claude_usage(FIXTURE_EMPTY)
        assert r.remaining_ms is None

    def test_with_limit_time(self):
        r = parse_claude_usage(FIXTURE_WITH_LIMIT)
        # 1h 5m = 3900 seconds
        assert r.remaining_ms == pytest.approx(3_900_000, rel=1e-3)

    def test_hours_only(self):
        r = parse_claude_usage(FIXTURE_NO_PCT)
        # 4h 0m = 14400 seconds
        assert r.remaining_ms == pytest.approx(14_400_000, rel=1e-3)

    def test_alt_keys_30m(self):
        r = parse_claude_usage(FIXTURE_ALT_KEYS)
        # "0h 30m" = 30m = 1800s
        assert r.remaining_ms == pytest.approx(1_800_000, rel=1e-3)


class TestParseClaudeUsageWeeklyStats:

    def test_weekly_messages_no_limit(self):
        r = parse_claude_usage(FIXTURE_STANDARD)
        assert r.weekly_messages_used == 234
        assert r.weekly_messages_limit is None  # no limit shown

    def test_weekly_messages_with_limit(self):
        r = parse_claude_usage(FIXTURE_WITH_LIMIT)
        assert r.weekly_messages_used == 892

    def test_weekly_tokens(self):
        r = parse_claude_usage(FIXTURE_STANDARD)
        # Input + output = 1,234,567 + 456,789 = 1,691,356
        assert r.weekly_tokens_used == 1_691_356

    def test_weekly_cost(self):
        r = parse_claude_usage(FIXTURE_STANDARD)
        assert r.weekly_cost_usd == pytest.approx(4.23, rel=1e-3)

    def test_zero_weekly(self):
        r = parse_claude_usage(FIXTURE_ZERO_USAGE)
        assert r.weekly_messages_used == 0
        assert r.weekly_cost_usd == pytest.approx(0.0)

    def test_no_weekly_returns_none(self):
        r = parse_claude_usage(FIXTURE_MINIMAL)
        assert r.weekly_messages_used is None
        assert r.weekly_cost_usd is None

    def test_alt_keys_weekly_tokens(self):
        r = parse_claude_usage(FIXTURE_ALT_KEYS)
        assert r.weekly_tokens_used == 9_876_543


class TestParseClaudeUsageEdgeCases:

    def test_returns_dataclass_always(self):
        """parse_claude_usage never raises — it always returns a result."""
        for fixture in [
            FIXTURE_STANDARD, FIXTURE_MINIMAL, FIXTURE_EMPTY,
            FIXTURE_ERROR, FIXTURE_NOISY, FIXTURE_ALT_KEYS,
        ]:
            result = parse_claude_usage(fixture)
            assert result is not None

    def test_error_field_on_empty(self):
        r = parse_claude_usage(FIXTURE_EMPTY)
        assert r.error is not None  # signals "no parseable content"

    def test_no_error_on_good_output(self):
        r = parse_claude_usage(FIXTURE_STANDARD)
        assert r.error is None

    def test_raw_text_preserved(self):
        r = parse_claude_usage(FIXTURE_MINIMAL)
        assert r.raw_text == FIXTURE_MINIMAL

    def test_messages_comma_separated_numbers(self):
        """Numbers like 1,234,567 must be parsed correctly."""
        r = parse_claude_usage(FIXTURE_STANDARD)
        assert r.weekly_tokens_used == 1_691_356  # 1,234,567 + 456,789
