import Pantograph.Frontend
import Test.Common

open Lean Pantograph Frontend

namespace Pantograph.Test.Frontend.Distil

abbrev TestM := TestT MetaM
abbrev Test := TestM Unit

private def collectSorrysFromSource (source: String) (options : Frontend.GoalCollectionOptions := {})
    : CoreM (List GoalState) := do
  let (context, state) ← do Frontend.createContextStateFromFile source (env? := ← getEnv)
  let m := show FrontendM _ from Frontend.mapCompilationSteps λ step => do
    return (step.before, ← Frontend.collectSorrys step options)
  let li ← m.run {} |>.run context |>.run' state
  let goalStates ← li.filterMapM λ (env, sorrys) => withEnv env do
    if sorrys.isEmpty then
      return .none
    let { state, .. } ← (Frontend.sorrysToGoalState sorrys).run'
    return .some state
  return goalStates

private def test_sorry_in_middle: Test := do
  let sketch := "
example : ∀ (n m: Nat), n + m = m + n := by
  intros n m
  sorry
  "
  let goalStates ← collectSorrysFromSource sketch
  let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
  checkEq "goals" ((← goalState.serializeGoals (options := {})).map (·.devolatilize)) #[
    {
      target := { pp? := "n + m = m + n" },
      vars := #[{
           userName := `n,
           type? := .some { pp? := "Nat" },
        }, {
           userName := `m,
           type? := .some { pp? := "Nat" },
        }
      ],
    }
  ]
  let .success st _ ← runTermElabMInMeta $ goalState.tryDraft .unfocus "have : 1 + 1 = 2 := by sorry\nsorry"
    | fail "Draft tactic failed"
  checkEq "goals" st.goals.length 2

private def test_sorry_in_coupled: Test := do
  let sketch := "
example : ∀ (y: Nat), ∃ (x: Nat), y + 1 = x := by
  intro y
  apply Exists.intro
  case h => sorry
  case w => sorry
  "
  let goalStates ← collectSorrysFromSource sketch
  let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
  checkEq "goals" ((← goalState.serializeGoals (options := {})).map (·.devolatilize)) #[
    {
      target := { pp? := "y + 1 = ?w" },
      vars := #[{
           userName := `y,
           type? := .some { pp? := "Nat" },
        }
      ],
    },
    {
      userName? := .some `w,
      target := { pp? := "Nat" },
      vars := #[{
           userName := `y,
           type? := .some { pp? := "Nat" },
        }
      ],
    }
  ]

private def test_sorry_with_local_instance (tacticMode : Bool) : Test := do
  let placeholder := if tacticMode then "by sorry" else "sorry"
  let sketch := s!"
def test (α : Type) [s : Inhabited α] : α := @Inhabited.default α s
example (α : Type) [Inhabited α] : α := {placeholder}
  "
  let goalStates ← collectSorrysFromSource sketch
  let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
  let result ← runTermElabMInMeta $ goalState.tryTactic .unfocus "exact test α"
  checkTrue "success" $ result matches .success ..
  match result with
  | .success .. => return ()
  | .failure messages =>
    let messages ← messages.mapM (·.toString)
    fail s!"Could not execute tactic {messages}"
  | .parseError e =>
    fail s!"Parse error: {e}"
  | .invalidAction e =>
    fail s!"Invalid action: {e}"

private def test_sorry_circular : Test := do
  let sketch := "
theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := by sorry
  "
  let goalStates ← collectSorrysFromSource sketch
  let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
  let result ← runTermElabMInMeta $ goalState.tryTactic .unfocus "exact test"
  checkTrue "failure" $ result matches .failure ..
  match result with
  | .success .. =>
    fail s!"This should not succeed"
  | .failure .. =>
    return ()
  | .parseError e =>
    fail s!"Parse error: {e}"
  | .invalidAction e =>
    fail s!"Invalid action: {e}"

private def test_environment_capture: Test := do
  let sketch := "
def mystery (n: Nat) := n + 1

example (n: Nat) : mystery n + 1 = n + 2 := sorry
  "
  let goalStates ← collectSorrysFromSource sketch
  let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
  checkEq "goals" ((← goalState.serializeGoals (options := {})).map (·.devolatilize)) #[
    {
      target := { pp? := "mystery n + 1 = n + 2" },
      vars := #[{
         userName := `n,
         type? := .some { pp? := "Nat" },
      }],
    }
  ]

private def test_capture_type_mismatch : Test := do
  let input := "
def mystery (k: Nat) : Nat := true
  "
  let options := { collectTypeErrors := true }
  let goalStates ← collectSorrysFromSource input options
  let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
  checkEq "goals" ((← goalState.serializeGoals).map (·.devolatilize)) #[
    {
      target := { pp? := "Nat" },
      vars := #[{
         userName := `k,
         type? := .some { pp? := "Nat" },
      }],
    }
  ]

def test_capture_type_mismatch_in_binder : Test := do
  let input := "
example (p: Prop) (h: (∀ (x: Prop), Nat) → p): p := h (λ (y: Nat) => 5)
  "
  let options := { collectTypeErrors := true }
  let goalStates ← collectSorrysFromSource input options
  let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
  checkEq "goals" ((← goalState.serializeGoals (options := {})).map (·.devolatilize)) #[]

private def test_distil_simple : Test := do
  let input := "
set_option pp.analyze true
theorem mystery : ∀ (p q : Prop), p ∨ q → q ∨ p := sorry
  "
  let [_dst@{ goalState := state }] ← distilSearchTargets (← getEnv) input
    | fail "Incorrect number of search states"
  let .success state _ ← (state.tryTactic .unfocus "intro p q").run' (ctx := defaultElabContext)
    | fail "`intro` failed"
  checkEq "goals" state.goals.length 1

private def test_distil_tail : Test := do
  let input := "
theorem mystery : ∀ (n m: Nat), n + m = m + n := by
  intros n m
  sorry
  "
  let [_dst@{ goalState := state }] ← distilSearchTargets (← getEnv) input { ignoreValues := false }
    | fail "Incorrect number of search states"
  checkEq "start" ((← state.serializeGoals {}).map (·.devolatilize))
    #[{
      target := { pp? := "n + m = m + n" },
      vars := #[{
           userName := `n,
           type? := .some { pp? := "Nat" },
        }, {
           userName := `m,
           type? := .some { pp? := "Nat" },
        }
      ],
    }]

private def test_distil_induction : Test := do
  let input := "
theorem mystery : ∀ (n m: Nat), n + m = m + n := by
  intros n m
  induction n with
  | zero =>
    have h1 : 0 + m = m := sorry
    sorry
  | succ n ih =>
    have h2 : n + m = m := sorry
    sorry
  "
  let [_dst@{ goalState := state }] ← distilSearchTargets (← getEnv) input { ignoreValues := false }
    | fail "Incorrect number of search states"
  let n' := .mkSimple "n✝"
  checkEq "start" ((← state.serializeGoals {}).map (·.devolatilize)) #[
    {
      target := { pp? := "n + 1 + m = m + (n + 1)" },
      vars := #[
        { var n' "Nat" with isInaccessible := true },
        var `m "Nat",
        var `n "Nat",
        var `ih "n + m = m + n",
        { var `h2 "n + m = m" with value? := .some { pp? := "?m.5" }},
      ],
    },
    {
      target := { pp? := "n + m = m" },
      vars := #[
        { var n' "Nat" with isInaccessible := true },
        var `m "Nat",
        var `n "Nat",
        var `ih "n + m = m + n",
      ],
    },
    {
      target := { pp? := "0 + m = m + 0" },
      vars := #[
        var `n "Nat",
        var `m "Nat",
        { var `h1 "0 + m = m" with value? := .some { pp? := "?m.2" }},
      ],
    },
    {
      target := { pp? := "0 + m = m" },
      vars := #[
        var `n "Nat",
        var `m "Nat",
      ],
    },
  ]
  where
  var (userName : Name) (type : String) : Protocol.Variable := {
    userName,
    type? := .some { pp? := type },
  }

private def test_distil_instance (tacticMode : Bool) : Test := do
  let placeholder := if tacticMode then "by sorry" else "sorry"
  let input := s!"
def test (α : Type) [s : Inhabited α] : α := @Inhabited.default α s
def mystery (α : Type) [Inhabited α] : α := {placeholder}
  "
  let [_dst@{ goalState := state }] ← distilSearchTargets (← getEnv) input { ignoreValues := false }
    | fail "Incorrect number of search states"
  checkEq "start" ((← state.serializeGoals {}).map (·.devolatilize))
    #[{
      target := { pp? := .some "α" },
      vars := #[
        {
          userName := `α,
          type? := .some { pp? := .some "Type" }
        },
        {
          userName := .mkSimple "inst✝",
          isInaccessible := true,
          type? := .some { pp? := .some "Inhabited α" }
        },
      ]
    }]
  let state? ← (state.tryTactic .unfocus "exact test α").run' (ctx := defaultElabContext)
  match state? with
  | .success state _ =>
    checkEq "goals" state.goals.length 0
    checkTrue "root" state.isSolved
  | .failure messages =>
    let messages ← messages.mapM (·.toString)
    checkEq "messages" messages #[];
    fail "failed"
  | .parseError e =>
    fail s!"Parse error: {e}"
  | .invalidAction e =>
    fail s!"Invalid action: {e}"

private def test_distil_environment_capture : Test := do
  let input := "
def mystery (n: Nat) := n + 1

theorem property (n: Nat) : mystery n + 1 = n + 2 := sorry
  "
  let [_dst@{ goalState := state }] ← distilSearchTargets (← getEnv) input { ignoreValues := false }
    | fail "Incorrect number of search states"
  let goals := (← state.serializeGoals).map (·.devolatilize)
  checkEq "goals" goals #[
    {
      target := { pp? := "mystery n + 1 = n + 2" },
      vars := #[{
         userName := `n,
         type? := .some { pp? := "Nat" },
      }],
    }
  ]
  checkFalse "root" state.isSolved
  let .success state _ ← runTermElabMInMeta do state.tryTactic .unfocus "rfl"
    | fail "Tactic block failed"
  checkTrue "root" state.isSolved

private def test_distil_circular : Test := do
  let input := "
theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := by sorry
  "
  let id := "test"
  let [_dst@{ goalState := state }] ← distilSearchTargets (← getEnv) input
    | fail "Incorrect number of search states"
  let result ← (state.tryTactic .unfocus s!"exact {id} p q hp hq").run' (ctx := defaultElabContext)
  match result with
  | .success .. =>
    fail s!"This should not succeed"
  | .failure messages =>
    let messages ← messages.mapM (·.toString)
    checkEq "failure" messages #[s!"{← getFileName}:0:0: error(lean.unknownIdentifier): Unknown identifier `{id}`\n"]
  | .parseError e =>
    fail s!"Parse error: {e}"
  | .invalidAction e =>
    fail s!"Invalid action: {e}"

private def test_distil_companion : Test := do
  let input := "
def f : Nat → Nat := sorry
def g : Nat → Nat := λ x => x + 1
theorem mystery (n : Nat) : f n = g n := sorry
  "
  let [_dst@{ goalState := state }] ← distilSearchTargets (← getEnv) input
    | fail "Incorrect number of search states"
  checkEq "start" ((← state.serializeGoals {}).map (·.devolatilize))
    #[{
      target := { pp? := .some "{ f // ∀ (n : Nat), f n = g n }" },
    }]

private def test_distil_multiple_cond : Test := do
  let input := "
def f : Nat → Nat := sorry
theorem mystery1 : f 1 = 1 := sorry
theorem mystery2 : f 2 = 2 := sorry
  "
  let [_dst@{ goalState := state }] ← distilSearchTargets (← getEnv) input
    | fail "Incorrect number of search states"
  checkEq "start" ((← state.serializeGoals {}).map (·.devolatilize))
    #[{
      target := { pp? := .some "{ f // f 1 = 1 ∧ f 2 = 2 }" },
    }]

private def test_distil_existing_value : Test := do
  let input := "
def f : Nat → Nat := λ x => x + sorry
theorem mystery1 : f 1 = 2 := sorry
theorem mystery2 : f 2 = 4 := sorry
  "
  let [_dst@{ goalState := state }] ← distilSearchTargets (← getEnv) input { ignoreValues := false }
    | fail "Incorrect number of search states"
  checkEq "start" ((← state.serializeGoals {}).map (·.devolatilize))
    #[
      {
        userName? := `f,
        vars := #[{ userName := `x, type? := .some { pp? := .some "Nat" } }]
        target := { pp? := .some "Nat" },
      },
      {
        target := { pp? := .some "(fun x => x + ?f) 1 = 2" },
      },
      {
        target := { pp? := .some "(fun x => x + ?f) 2 = 4" },
      },
    ]
  checkFalse "root" state.isSolved
  let .success state _ ← runTermElabMInMeta do state.tryTactic .unfocus "exact x; rfl; rfl"
    | fail "Tactic block failed"
  checkEq "goals" state.goals.length 0
  checkTrue "root" state.isSolved

/-- Tests handling of newline chars -/
private def test_distil_predicate : Test := do
  let input := "
structure Command where
  prog : String
  args : List String

  deriving Repr, DecidableEq

def p (s : String) : Prop := s = \"ls\"

theorem mystery (s : String) : p s := sorry
  "
  let [_dst@{ goalState := state }] ← distilSearchTargets (← getEnv) input
    | fail "Incorrect number of search states"
  checkTrue "has `p" <| (state.env.find? `p).isSome
  let state? ← (state.tryDraft .unfocus "by\n  unfold p\n  sorry").run' (ctx := defaultElabContext)
  match state? with
  | .success state _ =>
    checkEq "goals" state.goals.length 1
  | .failure messages =>
    let messages ← messages.mapM (·.toString)
    checkEq "messages" messages #[];
    fail "failed"
  | .parseError e =>
    fail s!"Parse error: {e}"
  | .invalidAction e =>
    fail s!"Invalid action: {e}"


def suite (env : Environment): List (String × IO LSpec.TestSeq) :=
  let tests := [
    ("sorry in middle", test_sorry_in_middle),
    ("sorry in coupled", test_sorry_in_coupled),
    ("sorry with local instances (term)", test_sorry_with_local_instance false),
    ("sorry with local instances (tactic)", test_sorry_with_local_instance true),
    ("sorry circular", test_sorry_circular),
    ("environment_capture", test_environment_capture),
    ("capture_type_mismatch", test_capture_type_mismatch),
    --("capture_type_mismatch_in_binder", test_capture_type_mismatch_in_binder),
    ("distil simple", test_distil_simple),
    ("distil tail", test_distil_tail),
    ("distil induction", test_distil_induction),
    ("distil instance (term)", test_distil_instance false),
    ("distil instance (true)", test_distil_instance true),
    ("distil environment capture", test_distil_environment_capture),
    ("distil circular", test_distil_circular),
    ("distil companion", test_distil_companion),
    ("distil multiple conditions", test_distil_multiple_cond),
    ("distil existing value", test_distil_existing_value),
    ("distil predicate", test_distil_predicate),
  ]
  tests.map (fun (name, test) => (name, runMetaMSeq env $ runTest test))
