defmodule QtraceTest do
  use ExUnit.Case
  import ExUnit.CaptureIO
  doctest Qtrace

  # Test helper functions with predictable performance
  defmodule TestFunctions do
    def function_1(sleep_ms) do
      :timer.sleep(sleep_ms)
    end

    def function_2(sleep_ms) do
      :timer.sleep(sleep_ms)
    end
  end

  describe "function tracing and quantile measurement" do
    test "can measure performance distribution with different quantiles" do
      session = Qtrace.start_session(:all)

      Qtrace.trace_function(session, TestFunctions, :function_1, 1)
      Qtrace.trace_function(session, TestFunctions, :function_2, 1)

      for _ <- 0..10 do
        TestFunctions.function_1(1)
        TestFunctions.function_1(5)
        TestFunctions.function_1(10)

        TestFunctions.function_2(10)
        TestFunctions.function_2(50)
        TestFunctions.function_2(100)
      end

      TestFunctions.function_1(50)
      TestFunctions.function_2(200)

      f1_p50 = Qtrace.get_quantile(session, 0.5, TestFunctions, :function_1, 1)
      assert f1_p50 >= 5000 and f1_p50 <= 6000

      f1_p95 = Qtrace.get_quantile(session, 0.95, TestFunctions, :function_1, 1)
      assert f1_p95 >= 10_000 and f1_p95 <= 12_000

      f1_p99 = Qtrace.get_quantile(session, 0.99, TestFunctions, :function_1, 1)
      assert f1_p99 >= 50_000

      f2_p50 = Qtrace.get_quantile(session, 0.5, TestFunctions, :function_2, 1)
      assert f2_p50 >= 50000 and f2_p50 <= 52_000

      f2_p95 = Qtrace.get_quantile(session, 0.95, TestFunctions, :function_2, 1)
      assert f2_p95 >= 90_000 and f2_p95 <= 100_000

      f2_p99 = Qtrace.get_quantile(session, 0.99, TestFunctions, :function_2, 1)
      assert f2_p99 >= 150_000 and f2_p99 <= 210_000
    end
  end

  describe "quantile validation" do
    test "rejects invalid quantile values" do
      session = Qtrace.start_session(:all)
      on_exit(fn -> Qtrace.stop_session(session) end)

      assert_raise ArgumentError, ~r/quantile must be between 0 and 1/, fn ->
        Qtrace.get_quantile(session, 0, TestFunctions, :fast_function, 1)
      end

      assert_raise ArgumentError, ~r/quantile must be between 0 and 1/, fn ->
        Qtrace.get_quantile(session, 1, TestFunctions, :fast_function, 1)
      end

      assert_raise ArgumentError, ~r/quantile must be between 0 and 1/, fn ->
        Qtrace.get_quantile(session, -0.1, TestFunctions, :fast_function, 1)
      end

      assert_raise ArgumentError, ~r/quantile must be between 0 and 1/, fn ->
        Qtrace.get_quantile(session, 1.5, TestFunctions, :fast_function, 1)
      end
    end
  end

  describe "tracing validation" do
    test "rejects tracing of untraceable modules" do
      session = Qtrace.start_session(:all)
      on_exit(fn -> Qtrace.stop_session(session) end)

      # Test tracing Qtrace itself
      assert_raise ArgumentError, ~r/module Qtrace can't be traced by Qtrace/, fn ->
        Qtrace.trace_function(session, Qtrace, :get_quantile, 5)
      end

      # Test tracing Qtrace.Tracer
      assert_raise ArgumentError, ~r/module Qtrace.Tracer can't be traced by Qtrace/, fn ->
        Qtrace.trace_function(session, Qtrace.Tracer, :get_quantile, 4)
      end

      # Test tracing DogSketch.SimpleDog
      assert_raise ArgumentError, ~r/module DogSketch.SimpleDog can't be traced by Qtrace/, fn ->
        Qtrace.trace_function(session, DogSketch.SimpleDog, :quantile, 2)
      end

      # Test that the error message mentions trace loops
      assert_raise ArgumentError, ~r/otherwise we could get a trace loop/, fn ->
        Qtrace.trace_function(session, Qtrace.Session, :some_function, 1)
      end
    end
  end

  describe "session management" do
    test "cleanup stops the tracer process" do
      session = Qtrace.start_session(:all)
      tracer_pid = session.pid

      # Verify tracer is alive
      assert Process.alive?(tracer_pid)

      # Clean up the tracer
      assert :ok = Qtrace.cleanup(session)

      # Give it a moment to terminate
      :timer.sleep(10)

      # Verify tracer is no longer alive
      refute Process.alive?(tracer_pid)
    end
  end

  describe "histogram functionality" do
    test "can generate histogram data from traced functions" do
      session = Qtrace.start_session(:all)
      on_exit(fn -> Qtrace.stop_session(session) end)

      Qtrace.trace_function(session, TestFunctions, :function_1, 1)

      # Generate some test data
      for _ <- 1..20 do
        TestFunctions.function_1(5)
        TestFunctions.function_1(10)
        TestFunctions.function_1(15)
      end

      histogram = Qtrace.get_histogram(session, TestFunctions, :function_1, 1)

      assert histogram != nil

      assert %Qtrace.Histogram{
               buckets: buckets,
               min: min_val,
               max: max_val,
               function: {TestFunctions, :function_1, 1},
               percentiles: percentiles
             } = histogram

      assert length(buckets) > 0
      assert min_val > 0
      assert max_val >= min_val

      # Check bucket structure
      first_bucket = List.first(buckets)
      assert %{range: {_start, _end}, midpoint: _mid, count: _count, label: _label} = first_bucket

      # Check percentiles structure
      assert %{p50: p50, p95: p95, p99: p99} = percentiles
      assert is_number(p50) and p50 > 0
      assert is_number(p95) and p95 > 0
      assert is_number(p99) and p99 > 0
      assert p50 <= p95
      assert p95 <= p99
    end

    test "returns nil for functions with no trace data" do
      session = Qtrace.start_session(:all)
      on_exit(fn -> Qtrace.stop_session(session) end)

      histogram = Qtrace.get_histogram(session, TestFunctions, :nonexistent_function, 1)
      assert histogram == nil
    end

    test "plot_histogram handles both valid data and no data cases" do
      session = Qtrace.start_session(:all)
      on_exit(fn -> Qtrace.stop_session(session) end)

      Qtrace.trace_function(session, TestFunctions, :function_2, 1)

      # Generate some test data
      for _ <- 1..10 do
        TestFunctions.function_2(20)
      end

      # Capture output and validate it contains expected elements
      output =
        capture_io(fn ->
          assert :ok = Qtrace.plot_histogram(session, TestFunctions, :function_2, 1)
        end)

      assert output =~ "Latency Histogram: QtraceTest.TestFunctions.function_2/1"
      assert output =~ "ms │"
      assert output =~ "Stats:"
      assert output =~ "Min:"
      assert output =~ "Max:"
      assert output =~ "Samples:"
      assert output =~ "P50:"
      assert output =~ "P95:"
      assert output =~ "P99:"

      # Test with no data - should show appropriate message
      no_data_output =
        capture_io(fn ->
          assert :ok = Qtrace.plot_histogram(session, TestFunctions, :nonexistent_function, 1)
        end)

      assert no_data_output =~
               "No data available for QtraceTest.TestFunctions.nonexistent_function/1"
    end

    test "histogram with diverse timing creates multiple buckets" do
      session = Qtrace.start_session(:all)
      on_exit(fn -> Qtrace.stop_session(session) end)

      Qtrace.trace_function(session, TestFunctions, :function_1, 1)

      # Generate test data with very diverse timing to create multiple buckets
      for _ <- 1..50 do
        # ~1ms
        TestFunctions.function_1(1)
        # ~10ms
        TestFunctions.function_1(10)
        # ~50ms
        TestFunctions.function_1(50)
        # ~100ms
        TestFunctions.function_1(100)
      end

      histogram = Qtrace.get_histogram(session, TestFunctions, :function_1, 1)

      # Should have multiple buckets due to diverse timing
      assert length(histogram.buckets) > 1

      # Test the plotting and validate output content
      output =
        capture_io(fn ->
          assert :ok = Qtrace.plot_histogram(session, TestFunctions, :function_1, 1)
        end)

      # Validate histogram structure and content
      assert output =~ "Latency Histogram: QtraceTest.TestFunctions.function_1/1"
      assert output =~ "ms │"
      # Should contain bar characters
      assert output =~ "█"
      # Should contain count numbers in parentheses
      assert output =~ "("
      assert output =~ "Stats:"
      assert output =~ "Min:"
      assert output =~ "Max:"
      assert output =~ "Samples: 200"
      assert output =~ "P50:"
      assert output =~ "P95:"
      assert output =~ "P99:"

      # Should show multiple different time values due to diverse timing
      time_values = Regex.scan(~r/(\d+\.?\d*)ms │/, output) |> Enum.map(&List.first/1)
      assert length(time_values) > 1
    end

    test "plot_all_histograms shows histograms for all traced functions" do
      session = Qtrace.start_session(:all)
      on_exit(fn -> Qtrace.stop_session(session) end)

      # Trace multiple functions
      Qtrace.trace_function(session, TestFunctions, :function_1, 1)
      Qtrace.trace_function(session, TestFunctions, :function_2, 1)

      # Generate test data for both functions
      for _ <- 1..10 do
        TestFunctions.function_1(5)
        TestFunctions.function_2(10)
      end

      # Capture output from plot_all_histograms
      output =
        capture_io(fn ->
          assert :ok = Qtrace.plot_all_histograms(session)
        end)

      # Should contain histograms for both functions
      assert output =~ "Latency Histogram: QtraceTest.TestFunctions.function_1/1"
      assert output =~ "Latency Histogram: QtraceTest.TestFunctions.function_2/1"

      # Should contain expected histogram elements for both
      assert output =~ "ms │"
      assert output =~ "█"
      assert output =~ "Stats:"
      assert output =~ "Min:"
      assert output =~ "Max:"
      assert output =~ "P50:"
      assert output =~ "P95:"
      assert output =~ "P99:"
    end

    test "plot_all_histograms handles no traced functions" do
      session = Qtrace.start_session(:all)
      on_exit(fn -> Qtrace.stop_session(session) end)

      # Don't trace any functions or execute any code

      output =
        capture_io(fn ->
          assert :ok = Qtrace.plot_all_histograms(session)
        end)

      assert output =~ "No traced functions found"
    end
  end
end
