defmodule Tracer.Test do
  use ExUnit.Case

  import Tracer
  import Tracer.Matcher
  alias Tracer.{Tool.Count, Tool.Duration, Tool.CallSeq, Tool.Display}

  setup do
    # kill server if alive? for a fresh test
    case Process.whereis(Tracer.Server) do
      nil -> :ok
      pid ->
        Process.exit(pid, :kill)
        :timer.sleep(10)
    end
    :ok
  end

  @tag :timing
  test "can add multiple probes" do
    {:ok, pid} = Tracer.start_server()
    assert Process.alive?(pid)
    test_pid = self()

    tool = Tracer.tool(Display, forward_to: test_pid)
    |> Tracer.Tool.add_probe(Tracer.probe(type: :call, process: :all,
                                  match: local do Map.new() -> :ok end))
    |> Tracer.Tool.add_probe(Tracer.probe(type: :gc, process: self()))
    |> Tracer.Tool.add_probe(Tracer.probe(type: :set_on_link, process: [self()]))
    |> Tracer.Tool.add_probe(Tracer.probe(type: :procs, process: [self()]))
    |> Tracer.Tool.add_probe(Tracer.probe(type: :receive, process: [self()]))
    |> Tracer.Tool.add_probe(Tracer.probe(type: :send, process: [self()]))
    |> Tracer.Tool.add_probe(Tracer.probe(type: :sched, process: [self()]))

    probes = Tracer.Tool.get_probes(tool)
    assert probes ==
            [
              %Tracer.Probe{enabled?: true, flags: [:arity, :timestamp],
              process_list: [:all], type: :call,
              clauses: [%Tracer.Clause{matches: 0, type: :call,
                        desc: "local do Map.new() -> :ok end",
                        flags: [:local],
                        match_specs: [{[], [], [:ok]}],
              mfa: {Map, :new, 0}}]},
             %Tracer.Probe{clauses: [], enabled?: true,
              flags: [:timestamp], process_list: [test_pid],
              type: :gc},
             %Tracer.Probe{clauses: [], enabled?: true,
              flags: [:timestamp],
              process_list: [test_pid], type: :set_on_link},
             %Tracer.Probe{clauses: [], enabled?: true,
              flags: [:timestamp],
              process_list: [test_pid], type: :procs},
             %Tracer.Probe{clauses: [], enabled?: true,
              flags: [:timestamp],
              process_list: [test_pid], type: :receive},
             %Tracer.Probe{clauses: [], enabled?: true,
              flags: [:timestamp],
              process_list: [test_pid], type: :send},
             %Tracer.Probe{clauses: [], enabled?: true,
              flags: [:timestamp],
              process_list: [test_pid], type: :sched}
            ]

      Tracer.run(tool)

      %{tracing: true} = :sys.get_state(Tracer.Server, 100)

      assert_receive :started_tracing

      res = :erlang.trace_info(test_pid, :flags)
      assert res == {:flags, [:arity, :garbage_collection, :running,
                      :set_on_link, :procs,
                      :call, :receive, :send, :timestamp]}
      res = :erlang.trace_info({Map, :new, 0}, :all)
      assert res == {:all,
                      [traced: :local,
                       match_spec: [{[], [], [:ok]}],
                       meta: false,
                       meta_match_spec: false,
                       call_time: false,
                       call_count: false]}

  end

  test "display tool" do
    test_pid = self()

    res = run(Display,
                     forward_to: test_pid,
                     process: test_pid,
                     match: local do Map.new() -> :ok end)
    assert res == :ok

    :timer.sleep(50)
    %{tracing: true} = :sys.get_state(Tracer.Server, 100)

    Map.new()

    assert_receive :started_tracing
    res = stop()
    assert res == :ok

    assert_receive %Tracer.EventCall{arity: 0, fun: :new, message: nil,
        mod: Map, pid: ^test_pid, ts: _}

    assert_receive {:done_tracing, :stop_command}
    # not expeting more events
    refute_receive(_)
  end

  test "count tool" do
    test_pid = self()

    res = run(Count,
                     process: test_pid,
                     forward_to: test_pid,
                     match: global do Map.new(%{a: a}); Map.new(%{b: b}) end)
    assert res == :ok

    :timer.sleep(50)

    %{tracing: true} = :sys.get_state(Tracer.Server, 100)

    Map.new(%{})
    Map.new(%{})
    Map.new(%{})
    Map.new(%{})
    Map.new(%{a: :foo})
    Map.new(%{a: :foo})
    Map.new(%{b: :bar})
    Map.new(%{b: :bar})
    Map.new(%{b: :bar})
    Map.new(%{b: :bar})
    Map.new(%{b: :bar})
    Map.new(%{b: :bar})

    assert_receive :started_tracing
    res = stop()
    assert res == :ok

    assert_receive %Count.Event{counts:
      [{[a: :foo], 2},
       {[b: :bar], 6}]}

    assert_receive {:done_tracing, :stop_command}
    # not expeting more events
    refute_receive(_)
  end

  def recur_len([], acc), do: acc
  def recur_len([_h | t], acc), do: recur_len(t, acc + 1)

  test "duration tool" do
    test_pid = self()

    res = run(Duration,
               process: test_pid,
               forward_to: test_pid,
               match: local Tracer.Test.recur_len(list, val))
    assert res == :ok

    :timer.sleep(50)

    %{tracing: true} = :sys.get_state(Tracer.Server, 100)

    recur_len([1, 2, 3, 4, 5], 0)
    recur_len([1, 2, 3, 5], 2)

    assert_receive :started_tracing
    assert_receive(%{pid: ^test_pid, mod: Tracer.Test, fun: :recur_len,
        arity: 2, duration: _, message: [[:list, [1, 2, 3, 4, 5]], [:val, 0]]})
    assert_receive(%{pid: ^test_pid, mod: Tracer.Test, fun: :recur_len,
        arity: 2, duration: _, message: [[:list, [1, 2, 3, 5]], [:val, 2]]})

    res = stop()
    assert res == :ok

    assert_receive {:done_tracing, :stop_command}
    # not expeting more events
    refute_receive(_)
  end

  @tag :timing
  test "call_seq tool" do
    test_pid = self()

    res = run(CallSeq,
               process: test_pid,
               forward_to: test_pid,
               show_args: true,
               show_return: true,
               ignore_recursion: false,
               start_match: Tracer.Test)
    assert res == :ok

    :timer.sleep(10)

    assert_receive :started_tracing

    recur_len([1, 2, 3, 4, 5], 0)

    :timer.sleep(10)
    res = stop()
    assert res == :ok

    assert_receive %CallSeq.Event{arity: 2, depth: 0, fun: :recur_len, message: [[[1, 2, 3, 4, 5], 0]], mod: Tracer.Test, pid: _, return_value: nil, type: :enter}
    assert_receive %CallSeq.Event{arity: 2, depth: 1, fun: :recur_len, message: [[[2, 3, 4, 5], 1]], mod: Tracer.Test, pid: _, return_value: nil, type: :enter}
    assert_receive %CallSeq.Event{arity: 2, depth: 2, fun: :recur_len, message: [[[3, 4, 5], 2]], mod: Tracer.Test, pid: _, return_value: nil, type: :enter}
    assert_receive %CallSeq.Event{arity: 2, depth: 3, fun: :recur_len, message: [[[4, 5], 3]], mod: Tracer.Test, pid: _, return_value: nil, type: :enter}
    assert_receive %CallSeq.Event{arity: 2, depth: 4, fun: :recur_len, message: [[[5], 4]], mod: Tracer.Test, pid: _, return_value: nil, type: :enter}
    assert_receive %CallSeq.Event{arity: 2, depth: 5, fun: :recur_len, message: [[[], 5]], mod: Tracer.Test, pid: _, return_value: nil, type: :enter}
    assert_receive %CallSeq.Event{arity: 2, depth: 5, fun: :recur_len, message: nil, mod: Tracer.Test, pid: _, return_value: 5, type: :exit}
    assert_receive %CallSeq.Event{arity: 2, depth: 4, fun: :recur_len, message: nil, mod: Tracer.Test, pid: _, return_value: 5, type: :exit}
    assert_receive %CallSeq.Event{arity: 2, depth: 3, fun: :recur_len, message: nil, mod: Tracer.Test, pid: _, return_value: 5, type: :exit}
    assert_receive %CallSeq.Event{arity: 2, depth: 2, fun: :recur_len, message: nil, mod: Tracer.Test, pid: _, return_value: 5, type: :exit}
    assert_receive %CallSeq.Event{arity: 2, depth: 1, fun: :recur_len, message: nil, mod: Tracer.Test, pid: _, return_value: 5, type: :exit}
    assert_receive %CallSeq.Event{arity: 2, depth: 0, fun: :recur_len, message: nil, mod: Tracer.Test, pid: _, return_value: 5, type: :exit}

    assert_receive {:done_tracing, :stop_command}
    # not expeting more events
    refute_receive(_)
  end

end
