///- --package .

arena_rows (test)

test arena_rows: prelude
///- --test static --symbol

:< testing
:! v10 = ()

:! v13 = ()

:! v15 = ()

:| competitor =
    | SpiralPolicy
    | SpiralRetreat
    | SpiralKiting
    | SpiralReach
    | SpiralPolicyAlly
    | Vbot
    | Nexbot
    | RandomControl

:= competitor_of (name : string) : option competitor = case_labels::case_of_label "" "-" name

:| monster =
    | Cyclops
    | Troll
    | Hunter
    | HuntingGround

:= monster_of (name : string) : option monster = case_labels::case_of_label "" "-" name

:== counted = monster * i32

:| monster_spec =
    | Lone : monster
    | Mix : list counted

:| column =
    | Run
    | Count
    | Experience
    | ClearedMs
    | LowestHealth
    | ActionsAllowed
    | ActionsDropped
    | Potions
    | Gold
    | Deaths
    | ForeignPotions
    | ForeignActions

:= economy_slot (c : column) : i32 =
    ?= c ?|
    | Potions => 10
    | Gold => 11
    | Deaths => 12
    | ForeignPotions => 13
    | _ => 14

:= column_slot (c : column) : i32 =
    ?= c ?|
    | Run => 2
    | Count => 4
    | Experience => 5
    | ClearedMs => 6
    | LowestHealth => 7
    | ActionsAllowed => 8
    | ActionsDropped => 9
    | _ => economy_slot c

:| row_rejection =
    | WrongWidth : i32
    | NotANumber : column
    | UnknownCompetitor : string
    | UnknownMonster : string
    | BadArmCell : i32

:| decoded t =
    | Decoded : t
    | Rejected : row_rejection

:= and_then \* a b. (next : a -> decoded b) (d : decoded a) : decoded b =
    ?= d ?|
    | Decoded x => next x
    | Rejected r => Rejected r

:= cell (cells : array string) (c : column) : string = am'::index (column_slot c) cells

:= number (cells : array string) (c : column) : decoded i32 =
    ?= convert::try_convert (cell cells c) ?|
    | Some n => Decoded n
    | None => Rejected (NotANumber c)

:= named \* t. (found : option t) (rejection : row_rejection) : decoded t =
    ?= found ?|
    | Some x => Decoded x
    | None => Rejected rejection

:= monster_count (name : string) (count : string) : option (monster * i32) =
    ?= monster_of name, convert::try_convert count ?|
    | Some m, Some n => Some (m, n)
    | _ => None

:= counted_monster (part : string) : option (monster * i32) =
    := fields = part |> sm'::split ":" |> array
    ? am'::length fields <> 2 ?> None |: monster_count (am'::index 0 fields) (am'::index 1 fields)

:~ ^ mixed_from (parts : array string) (i : i32) (found : list counted) : option monster_spec =
    ? i < 0 ?> Some (Mix found)
    |:
        ?= counted_monster (am'::index i parts) ?|
        | Some pair => mixed_from parts (i - 1) (pair :: found)
        | None => None

:= lone_spec (name : string) : option monster_spec =
    ?= monster_of name ?|
    | Some m => Some (Lone m)
    | None => None

:= spec_of (name : string) : option monster_spec =
    := parts = name |> sm'::split "," |> array
    ? sm'::contains ":" name ?> mixed_from parts (am'::length parts - 1) [] |: lone_spec name

:== run_head = { timestamp : string; competitor : competitor; run : i32; monster : monster_spec; count : i32 }

:| clear_time =
    | Cleared : i32
    | NotCleared

:== client = { experience : i32; lowest_health : i32; actions_allowed : i32; actions_dropped : i32 }

:| client_data =
    | Client : client
    | NoClientData

:== base_run = { head : run_head; client : client_data; cleared : clear_time }

:== economy = { potions : i32; gold : i32; deaths : i32 }

:| foreign =
    | NotRecorded
    | Recorded : i32 * i32

:| arena_row v =
    | Base :: base_run -> arena_row v10
    | WithEconomy :: base_run * economy -> arena_row v13
    | WithForeign :: base_run * economy * foreign -> arena_row v15

:| some_arena_row =
    | AtV10 : arena_row v10
    | AtV13 : arena_row v13
    | AtV15 : arena_row v15

:= head_with_run
    (cells : array string) (who : competitor) (spec : monster_spec) (run : i32) (n : i32)
    : decoded run_head
    =
    Decoded { timestamp = am'::index 0 cells; competitor = who; run = run; monster = spec; count = n }

:= head_with_spec (cells : array string) (who : competitor) (spec : monster_spec) (run : i32) : decoded run_head =
    number cells Count |> and_then (head_with_run cells who spec run)

:= head_with_competitor (cells : array string) (who : competitor) (spec : monster_spec) : decoded run_head =
    number cells Run |> and_then (head_with_spec cells who spec)

:= head_of (cells : array string) (who : competitor) : decoded run_head =
    := name = am'::index 3 cells
    named (spec_of name) (UnknownMonster name) |> and_then (head_with_competitor cells who)

:= client_done (e : i32) (l : i32) (a : i32) (d : i32) : decoded client_data =
    Decoded (Client { experience = e; lowest_health = l; actions_allowed = a; actions_dropped = d })

:= client_with (cells : array string) (e : i32) (l : i32) (a : i32) : decoded client_data =
    number cells ActionsDropped |> and_then (client_done e l a)

:= client_from (cells : array string) (e : i32) (l : i32) : decoded client_data =
    number cells ActionsAllowed |> and_then (client_with cells e l)

:= client_read (cells : array string) (e : i32) : decoded client_data =
    number cells LowestHealth |> and_then (client_from cells e)

:= client_of (cells : array string) : decoded client_data =
    ? cell cells Experience = "" ?> Decoded NoClientData |: number cells Experience |> and_then (client_read cells)

:= cleared_of (cells : array string) : decoded clear_time =
    ? cell cells ClearedMs = "" ?> Decoded NotCleared |: number cells ClearedMs |> and_then \ ms => Decoded (Cleared ms)

:= base_with_client (cells : array string) (head : run_head) (client : client_data) : decoded base_run =
    cleared_of cells |> and_then (\ cleared => Decoded { head = head; client = client; cleared = cleared })

:= base_with_head (cells : array string) (head : run_head) : decoded base_run =
    client_of cells |> and_then (base_with_client cells head)

:= base_of (cells : array string) : decoded base_run =
    := name = am'::index 1 cells
    named (competitor_of name) (UnknownCompetitor name) |> and_then (head_of cells) |> and_then (base_with_head cells)

:= economy_with (cells : array string) (p : i32) (g : i32) : decoded economy =
    number cells Deaths |> and_then (\ d => Decoded { potions = p; gold = g; deaths = d })

:= economy_of (cells : array string) : decoded economy =
    number cells Potions |> and_then (\ p => number cells Gold |> and_then (economy_with cells p))

:= foreign_with (cells : array string) (p : i32) : decoded foreign =
    number cells ForeignActions |> and_then (\ a => Decoded (Recorded (p, a)))

:= foreign_of (cells : array string) : decoded foreign =
    ? cell cells ForeignPotions = "" && cell cells ForeignActions = "" ?> Decoded NotRecorded
    |: number cells ForeignPotions |> and_then (foreign_with cells)

:= economy_row (cells : array string) (b : base_run) : decoded some_arena_row =
    economy_of cells |> and_then (\ e => Decoded (AtV13 (WithEconomy (b, e))))

:= foreign_row (cells : array string) (b : base_run) : decoded some_arena_row =
    economy_of cells |> and_then (\ e => foreign_of cells |> and_then (\ f => Decoded (AtV15 (WithForeign (b, e, f)))))

:= row_by_width (cells : array string) (width : i32) (b : base_run) : decoded some_arena_row =
    ? width = 10 ?> Decoded (AtV10 (Base b)) |? width = 13 ?> economy_row cells b |: foreign_row cells b

:= row_of_cells (cells : array string) : decoded some_arena_row =
    := width : i32 = am'::length cells
    ? width = 10 || width = 13 || width = 15 ?> base_of cells |> and_then (row_by_width cells width)
    |: Rejected (WrongWidth width)

:= row_of_line (line : string) : decoded some_arena_row = line |> sm'::split "\t" |> array |> row_of_cells

:= base_in \* v. (row : arena_row v) : base_run =
    ?= row ?|
    | Base b => b
    | WithEconomy (b, _) => b
    | WithForeign (b, _, _) => b

:= base_of_row (r : some_arena_row) : base_run =
    ?= r ?|
    | AtV10 row => base_in row
    | AtV13 row => base_in row
    | AtV15 row => base_in row

:= version_of (r : some_arena_row) : i32 =
    ?= r ?|
    | AtV10 _ => 10
    | AtV13 _ => 13
    | AtV15 _ => 15

:= foreign_counts (row : arena_row v15) : foreign =
    ?= row ?|
    | WithForeign (_, _, f) => f

:= rejection_of \* t. (d : decoded t) : option row_rejection =
    ?= d ?|
    | Decoded _ => None
    | Rejected r => Some r

:= summary (d : decoded some_arena_row) : i32 * i32 * clear_time =
    ?= d ?|
    | Decoded r => version_of r, (base_of_row r)::head::count, (base_of_row r)::cleared
    | Rejected _ => 0, 0, NotCleared

:= v10_line () : string = "2026-10-13T08:54:41\tspiral-policy\t1\ttroll\t3\t90\t98750\t169\t21\t8"

:= v13_uncleared_line () : string = "2026-10-14T07:31:54\tspiral-retreat\t1\tcyclops\t3\t675\t\t118\t21\t10\t9\t66\t0"

:= v15_line () : string = "2026-10-23T03:20:34\tspiral-retreat\t1\tcyclops\t3\t675\t97797\t113\t24\t6\t15\t53\t0\t\t"

:== row_summary = i32 * i32 * clear_time

:= schema_summaries (first : string) : row_summary * row_summary * row_summary =
    summary (row_of_line first), summary (row_of_line (v13_uncleared_line ())), summary (row_of_line (v15_line ()))

:= expected_summaries () : row_summary * row_summary * row_summary =
    (10i32, 3i32, Cleared 98750i32), (13i32, 3i32, NotCleared), (15i32, 3i32, Cleared 97797i32)

each_schema_decodes_to_its_version (test)

test each_schema_decodes_to_its_version: passed
///- --test --symbol
///> rust

:= ~line = v10_line ()
schema_summaries line |> _assert_eq (expected_summaries ())
__assert_eq { expected = ((10, 3, Cleared(98750)), (13, 3, NotCleared), 15, 3, Cleared(97797)) }
0
:= unknown_bot_line () : string = "2026-10-13T08:54:41\topenbot\t1\ttroll\t3\t90\t98750\t169\t21\t8"

:= rejections (short : string) : option row_rejection * option row_rejection =
    rejection_of (row_of_line short), rejection_of (row_of_line (unknown_bot_line ()))

a_bad_row_is_rejected_with_its_reason (test)

test a_bad_row_is_rejected_with_its_reason: passed
///- --test --symbol
///> rust

:= ~short = "a\tb\tc"
rejections short |> _assert_eq (Some (WrongWidth 3i32), Some (UnknownCompetitor "openbot"))
__assert_eq { expected = (Some(WrongWidth(3)), Some(UnknownCompetitor(openbot))) }
0
:= mixed_line () : string = "2026-10-20T01:00:00\tspiral-policy\t2\tcyclops:1,troll:2\t3\t\t\t\t\t\t1\t2\t0"

:= spec_size (s : monster_spec) : i32 =
    ?= s ?|
    | Mix parts => listm::length parts
    | Lone _ => 1

:= mix_size (d : decoded some_arena_row) : i32 =
    ?= d ?|
    | Decoded r => spec_size (base_of_row r)::head::monster
    | Rejected _ => 0

a_mixed_camp_and_missing_client_data_decode (test)

test a_mixed_camp_and_missing_client_data_decode: passed
///- --test --symbol
///> rust

:= ~line = mixed_line ()
mix_size (row_of_line line) |> _assert_eq 2i32
__assert_eq { expected = 2 }
0
:! log_name = string

:= log_of (path : string) : log_name =
    := at : i32 = sm'::last_index_of "/" path
    log_name (sm'::substring (at + 1) (sm'::length path - at - 1) path)

:| arm =
    | Ally
    | Arm : competitor

:= arm_competitor (name : string) : option arm =
    ?= competitor_of name ?|
    | Some c => Some (Arm c)
    | None => None

:= arm_of (name : string) : option arm = ? name = "ally" ?> Some Ally |: arm_competitor name

:== arm_step = { log : log_name; arm : arm; steps : i32; taken : i32; potions : i32 }

:= arm_number (cells : array string) (slot : i32) : decoded i32 =
    ?= convert::try_convert (am'::index slot cells) ?|
    | Some n => Decoded n
    | None => Rejected (BadArmCell slot)

:= arm_done (cells : array string) (a : arm) (steps : i32) (taken : i32) (potions : i32) : decoded arm_step =
    Decoded { log = log_of (am'::index 0 cells); arm = a; steps = steps; taken = taken; potions = potions }

:= arm_with (cells : array string) (a : arm) (steps : i32) : decoded arm_step =
    arm_number cells 3 |> and_then (\ taken => arm_number cells 4 |> and_then (arm_done cells a steps taken))

:= arm_read (cells : array string) (a : arm) : decoded arm_step = arm_number cells 2 |> and_then (arm_with cells a)

:= arm_step_of_cells (cells : array string) : decoded arm_step =
    := name = am'::index 1 cells
    ? am'::length cells <> 5 ?> Rejected (WrongWidth (am'::length cells))
    |: named (arm_of name) (UnknownCompetitor name) |> and_then (arm_read cells)

:= arm_step_of_line (line : string) : decoded arm_step = line |> sm'::split "\t" |> array |> arm_step_of_cells

:= arm_line () : string =
    "C:/Users/i574n/tibia-local/arena-logs/20261023-183122-cyclops-spiral-retreat-1.log"
    ^^ "\tspiral-retreat\t10\t76\t18"

:= log_text (log_name n : log_name) : string = n

:= arm_summary (d : decoded arm_step) : string * i32 * i32 =
    ?= d ?|
    | Decoded (s : arm_step) => log_text s::log, s::steps, s::potions
    | Rejected _ => "", 0, 0

an_arm_step_keeps_only_the_log_basename (test)

test an_arm_step_keeps_only_the_log_basename: passed
///- --test --symbol
///> rust

:= ~line = arm_line ()
arm_summary (arm_step_of_line line) |> _assert_eq ("20261023-183122-cyclops-spiral-retreat-1.log", 10i32, 18i32)
__assert_eq { expected = (20261023-183122-cyclops-spiral-retreat-1.log, 10, 18) }
0