import Lean

namespace CircuitBreaker

-- ANCHOR: config
structure Config where
  threshold : Nat
  timeout : Nat
  deriving DecidableEq, Repr
-- ANCHOR_END: config

-- ANCHOR: state
inductive State where
  | closed (failures : Nat) : State
  | opened (openedAt : Nat) : State
  | halfOpen : State
  deriving DecidableEq, Repr
-- ANCHOR_END: state

-- ANCHOR: event
inductive Event where
  | success : Event
  | failure (time : Nat) : Event
  | tick (time : Nat) : Event
  | probeSuccess (time : Nat) : Event
  | probeFailure (time : Nat) : Event
  deriving DecidableEq, Repr
-- ANCHOR_END: event

-- ANCHOR: step
def step (cfg : Config) (s : State) (e : Event) : State :=
  match s, e with
  | .closed _, .success => .closed 0
  | .closed failures, .failure time =>
    if failures + 1 >= cfg.threshold then .opened time
    else .closed (failures + 1)
  | .opened openedAt, .tick time =>
    if time - openedAt >= cfg.timeout then .halfOpen
    else .opened openedAt
  | .halfOpen, .probeSuccess _ => .closed 0
  | .halfOpen, .probeFailure time => .opened time
  | s, _ => s
-- ANCHOR_END: step

def initial : State := .closed 0

-- ANCHOR: invariant
def Invariant (cfg : Config) : State → Prop
  | .closed failures => failures < cfg.threshold
  | .opened _ => True
  | .halfOpen => True

theorem initial_invariant (cfg : Config) (h : cfg.threshold > 0) :
    Invariant cfg initial := h

theorem step_preserves_invariant (cfg : Config) (s : State) (e : Event)
    (hinv : Invariant cfg s) (hpos : cfg.threshold > 0) :
    Invariant cfg (step cfg s e) := by
  cases s with
  | closed failures =>
    cases e with
    | success => simp [step, Invariant, hpos]
    | failure time =>
      simp only [step]
      split
      · simp [Invariant]
      · simp [Invariant]; omega
    | tick _ => simp [step, Invariant]; exact hinv
    | probeSuccess _ => simp [step, Invariant]; exact hinv
    | probeFailure _ => simp [step, Invariant]; exact hinv
  | opened openedAt =>
    cases e with
    | tick time =>
      simp only [step]
      split <;> simp [Invariant]
    | _ => simp [step, Invariant]
  | halfOpen =>
    cases e with
    | probeSuccess _ => simp [step, Invariant, hpos]
    | probeFailure _ => simp [step, Invariant]
    | _ => simp [step, Invariant]
-- ANCHOR_END: invariant

-- ANCHOR: theorems
theorem success_resets (cfg : Config) (f : Nat) :
    step cfg (.closed f) .success = .closed 0 := rfl

theorem threshold_trips (cfg : Config) (f t : Nat) (h : f + 1 >= cfg.threshold) :
    step cfg (.closed f) (.failure t) = .opened t := by simp [step, h]

theorem below_threshold_increments (cfg : Config) (f t : Nat) (h : f + 1 < cfg.threshold) :
    step cfg (.closed f) (.failure t) = .closed (f + 1) := by
  simp [step]; omega

theorem timeout_transitions (cfg : Config) (o t : Nat) (h : t - o >= cfg.timeout) :
    step cfg (.opened o) (.tick t) = .halfOpen := by simp [step, h]

theorem probe_success_closes (cfg : Config) (t : Nat) :
    step cfg .halfOpen (.probeSuccess t) = .closed 0 := rfl

theorem probe_failure_reopens (cfg : Config) (t : Nat) :
    step cfg .halfOpen (.probeFailure t) = .opened t := rfl
-- ANCHOR_END: theorems

-- ANCHOR: uniformity
def sameStateKind : State → State → Bool
  | .closed _, .closed _ => true
  | .opened _, .opened _ => true
  | .halfOpen, .halfOpen => true
  | _, _ => false

def sameEventKind : Event → Event → Bool
  | .success, .success => true
  | .failure _, .failure _ => true
  | .tick _, .tick _ => true
  | .probeSuccess _, .probeSuccess _ => true
  | .probeFailure _, .probeFailure _ => true
  | _, _ => false

structure ComparisonResults where
  thresholdReached : Bool
  timeoutElapsed : Bool
  deriving DecidableEq, Repr

def getComparisons (cfg : Config) (s : State) (e : Event) : ComparisonResults :=
  match s, e with
  | .closed f, .failure _ => ⟨f + 1 >= cfg.threshold, false⟩
  | .opened o, .tick t => ⟨false, t - o >= cfg.timeout⟩
  | _, _ => ⟨false, false⟩

theorem uniformity (cfg₁ cfg₂ : Config) (s₁ s₂ : State) (e₁ e₂ : Event)
    (hsame_state : sameStateKind s₁ s₂)
    (hsame_event : sameEventKind e₁ e₂)
    (hsame_cmp : getComparisons cfg₁ s₁ e₁ = getComparisons cfg₂ s₂ e₂) :
    sameStateKind (step cfg₁ s₁ e₁) (step cfg₂ s₂ e₂) := by
  -- Step 1: Case split on state constructors. Off-diagonal cases (e.g., closed vs opened)
  -- are contradictions since hsame_state requires matching constructors.
  cases s₁ <;> cases s₂ <;> simp_all [sameStateKind]
  -- Step 2: For each diagonal case (closed/closed, opened/opened, halfOpen/halfOpen),
  -- split on event constructors. Again, off-diagonal cases contradict hsame_event.
  all_goals cases e₁ <;> cases e₂ <;> simp_all [sameEventKind, step, getComparisons]
  -- Step 3: Two cases remain where step branches on comparisons:
  -- (closed, failure) branches on threshold check
  case closed.closed.failure.failure f₁ f₂ t₁ t₂ =>
    -- hsame_cmp says both threshold comparisons have the same boolean result.
    -- Case split on whether each threshold is reached.
    by_cases h₁ : f₁ + 1 >= cfg₁.threshold <;>
    by_cases h₂ : f₂ + 1 >= cfg₂.threshold <;>
    -- When h₁ and h₂ disagree, hsame_cmp gives a contradiction.
    -- When they agree, both step to the same constructor.
    simp only [h₂, ↓reduceIte] <;> simp_all
  -- (opened, tick) branches on timeout check
  case opened.opened.tick.tick o₁ o₂ t₁ t₂ =>
    -- Same logic: hsame_cmp forces timeout comparisons to agree.
    by_cases h₁ : t₁ - o₁ >= cfg₁.timeout <;>
    by_cases h₂ : t₂ - o₂ >= cfg₂.timeout <;>
    simp only [h₂, ↓reduceIte] <;> simp_all
-- ANCHOR_END: uniformity

-- ANCHOR: correctness
theorem correctness (cfg : Config) (hpos : cfg.threshold > 0) :
    (Invariant cfg initial) ∧
    (∀ s e, Invariant cfg s → Invariant cfg (step cfg s e)) ∧
    (∀ cfg₂ s₁ s₂ e₁ e₂,
      sameStateKind s₁ s₂ → sameEventKind e₁ e₂ →
      getComparisons cfg s₁ e₁ = getComparisons cfg₂ s₂ e₂ →
      sameStateKind (step cfg s₁ e₁) (step cfg₂ s₂ e₂)) :=
  ⟨initial_invariant cfg hpos,
   fun s e hinv => step_preserves_invariant cfg s e hinv hpos,
   fun _ _ _ _ _ hs he hc => uniformity cfg _ _ _ _ _ hs he hc⟩
-- ANCHOR_END: correctness

-- ANCHOR: bounds
structure Bounds where
  maxThreshold : Nat := 4
  maxTimeout : Nat := 10
  maxTime : Nat := 20
-- ANCHOR_END: bounds

def enumerateStates (threshold maxTime : Nat) : List State :=
  (List.range threshold).map .closed ++
  (List.range (maxTime + 1)).map .opened ++
  [.halfOpen]

def enumerateEvents (maxTime : Nat) : List Event :=
  [.success] ++
  (List.range (maxTime + 1)).map .failure ++
  (List.range (maxTime + 1)).map .tick ++
  (List.range (maxTime + 1)).map .probeSuccess ++
  (List.range (maxTime + 1)).map .probeFailure

-- ANCHOR: testcase
structure TestCase where
  threshold : Nat
  timeout : Nat
  state : State
  event : Event
  expected : State
  deriving Repr
-- ANCHOR_END: testcase

def exhaustiveTests (b : Bounds) : List TestCase := Id.run do
  let mut tests : List TestCase := []
  for threshold in List.range' 1 b.maxThreshold do
    for timeout in List.range' 1 b.maxTimeout do
      let cfg : Config := ⟨threshold, timeout⟩
      for s in enumerateStates threshold b.maxTime do
        for e in enumerateEvents b.maxTime do
          tests := ⟨threshold, timeout, s, e, step cfg s e⟩ :: tests
  return tests

open Lean in
def State.toJson : State → Json
  | .closed n => .mkObj [("Closed", .num n)]
  | .opened n => .mkObj [("Open", .num n)]
  | .halfOpen => .str "HalfOpen"

open Lean in
def Event.toJson : Event → Json
  | .success => .str "Success"
  | .failure n => .mkObj [("Failure", .num n)]
  | .tick n => .mkObj [("Tick", .num n)]
  | .probeSuccess n => .mkObj [("ProbeSuccess", .num n)]
  | .probeFailure n => .mkObj [("ProbeFailure", .num n)]

open Lean in
def TestCase.toJson (tc : TestCase) : Json :=
  .mkObj [
    ("threshold", .num tc.threshold),
    ("timeout", .num tc.timeout),
    ("state", tc.state.toJson),
    ("event", tc.event.toJson),
    ("expected", tc.expected.toJson)
  ]

open Lean in
def exportTests (tests : List TestCase) : Json :=
  .arr (tests.map TestCase.toJson).toArray

def writeTests (path : System.FilePath) (b : Bounds) : IO Unit := do
  let tests := exhaustiveTests b
  IO.FS.writeFile path (exportTests tests).compress
  IO.println s!"Exported {tests.length} test cases to {path}"

-- ANCHOR: simulate
def simulate (cfg : Config) (events : List Event) : List State :=
  let rec go (s : State) (es : List Event) (acc : List State) : List State :=
    match es with
    | [] => (s :: acc).reverse
    | e :: rest => go (step cfg s e) rest (s :: acc)
  go initial events []
-- ANCHOR_END: simulate

-- ANCHOR: example
#eval do
  let cfg : Config := ⟨3, 100⟩
  let events := [Event.failure 10, .failure 20, .failure 30, .tick 50, .tick 150, .probeSuccess 160]
  let trace := simulate cfg events
  IO.println s!"Trace: {repr trace}"
-- ANCHOR_END: example

def defaultBounds : Bounds := { maxThreshold := 4, maxTimeout := 10, maxTime := 20 }

#eval do
  let n := (exhaustiveTests defaultBounds).length
  IO.println s!"Exhaustive tests: {n} cases"

#eval writeTests "examples/circuit-breaker/testdata/exhaustive_tests.json" defaultBounds

end CircuitBreaker
