# typed: false
require "rails_helper"

RSpec.describe Stats::Equation do
  let(:sample_records) do
    [
      Stats::DataPoint.new(x: 100, y: 1_000_000),
      Stats::DataPoint.new(x: 200, y: 2_000_000),
      Stats::DataPoint.new(x: 300, y: 3_000_000),
      Stats::DataPoint.new(x: 400, y: 4_000_000),
      Stats::DataPoint.new(x: 500, y: 5_000_000),
    ]
  end

  describe "Stats::Equation (abstract base class)" do
    describe "Stats::Helpers.format_number" do
      it "formats regular numbers with appropriate precision" do
        expect(Stats::Helpers.format_number(123.456)).to eq("123")
        expect(Stats::Helpers.format_number(1.23456)).to eq("1.23")
        expect(Stats::Helpers.format_number(0.00123)).to eq("1.23e-3")
      end

      it "formats zero correctly" do
        expect(Stats::Helpers.format_number(0.0)).to eq("0.0")
      end

      it "formats very large numbers using scientific notation" do
        expect(Stats::Helpers.format_number(1234567890.0)).to eq("1.23e9")
        expect(Stats::Helpers.format_number(999999999.0)).to eq("10.0e8")
      end

      it "formats very small numbers using scientific notation" do
        expect(Stats::Helpers.format_number(0.000001)).to eq("1.0e-6")
        expect(Stats::Helpers.format_number(0.0000123)).to eq("1.23e-5")
      end

      it "respects significant figures parameter" do
        expect(Stats::Helpers.format_number(123.456, 2)).to eq("123")
        expect(Stats::Helpers.format_number(123.456, 4)).to eq("123.5")
      end

      it "handles negative numbers" do
        expect(Stats::Helpers.format_number(-123.456)).to eq("-123")
        expect(Stats::Helpers.format_number(-0.000001)).to eq("-1.0e-6")
      end
    end
  end

  describe "Equation evaluation correctness" do
    # Simple data with known mathematical relationships for testing
    let(:simple_records) do
      [Stats::DataPoint.new(x: 0, y: 0), Stats::DataPoint.new(x: 10, y: 100)]
    end

    describe "PolynomialEquation evaluation correctness" do
      let(:normalizer) { Stats::LinearNormalizer.new(simple_records) }

      context "with constant polynomial (y = c)" do
        let(:equation) do
          Stats::PolynomialEquation.new(normalizer.x, normalizer.y, [0.42])
        end

        it "returns constant value for any x" do
          # With y_scale = 100, coefficient 0.42 becomes 42.0 in original space
          expected_value = 0.42 * 100 + 0 # coefficient * y_scale + y_min
          expect(equation.evaluate(0.0)).to be_within(0.001).of(expected_value)
          expect(equation.evaluate(5.0)).to be_within(0.001).of(expected_value)
          expect(equation.evaluate(10.0)).to be_within(0.001).of(expected_value)
        end
      end

      context "with linear polynomial (y = mx + b)" do
        # For simple_records: x_scale = 10, y_scale = 100
        # To get y = 2x + 3 in original space, we need to work backwards
        let(:equation) do
          Stats::PolynomialEquation.new(normalizer.x, normalizer.y, [0.03, 0.2])
        end

        it "evaluates linear equation correctly" do
          # These tests verify the mathematical relationship, not exact values
          # since the coefficients are in normalized space
          y_0 = equation.evaluate(0.0)
          y_1 = equation.evaluate(1.0)
          y_5 = equation.evaluate(5.0)
          y_10 = equation.evaluate(10.0)

          # Verify it's actually linear (constant slope)
          slope_0_to_1 = y_1 - y_0
          slope_1_to_5 = (y_5 - y_1) / 4
          slope_5_to_10 = (y_10 - y_5) / 5

          expect(slope_0_to_1).to be_within(0.001).of(slope_1_to_5)
          expect(slope_1_to_5).to be_within(0.001).of(slope_5_to_10)
        end

        it "handles boundary values correctly" do
          # Just verify the values are finite and in ascending order for positive slope
          result_min = equation.evaluate(0.0) # x_min
          result_max = equation.evaluate(10.0) # x_max

          expect(result_min).to be_finite
          expect(result_max).to be_finite
          expect(result_max).to be > result_min # positive slope
        end
      end

      context "with quadratic polynomial (y = ax² + bx + c)" do
        let(:equation) do
          Stats::PolynomialEquation.new(
            normalizer.x,
            normalizer.y,
            [0.01, 0.02, 0.01],
          )
        end

        it "evaluates quadratic equation correctly" do
          # Test that it maintains quadratic relationship
          x_values = [0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
          y_values = x_values.map { |x| equation.evaluate(x) }

          # For quadratic, second differences should be approximately constant
          first_diffs =
            (1...y_values.length).map { |i| y_values[i] - y_values[i - 1] }
          second_diffs =
            (1...first_diffs.length).map do |i|
              first_diffs[i] - first_diffs[i - 1]
            end

          # All second differences should be approximately equal
          second_diffs.each_with_index do |diff, i|
            next if i == 0
            expect(diff).to be_within(0.01).of(second_diffs[0])
          end
        end
      end

      context "with negative coefficients" do
        let(:equation) do
          Stats::PolynomialEquation.new(
            normalizer.x,
            normalizer.y,
            [0.1, -0.01],
          )
        end

        it "handles negative slopes correctly" do
          y_0 = equation.evaluate(0.0)
          y_5 = equation.evaluate(5.0)
          y_10 = equation.evaluate(10.0)

          # Negative slope means decreasing values
          expect(y_5).to be < y_0
          expect(y_10).to be < y_5
        end
      end
    end

    describe "TransformedEquation evaluation correctness" do
      context "LogarithmicEquation" do
        let(:log_records) do
          [
            Stats::DataPoint.new(x: 1, y: 0),
            Stats::DataPoint.new(x: 10, y: 100),
          ]
        end
        let(:normalizer) { Stats::LogarithmicNormalizer.new(log_records) }

        context "with simple coefficients" do
          # For logarithmic equations, the coefficients are in normalized space
          # y = norm_slope * ln(x) + norm_intercept, then denormalized
          let(:equation) do
            Stats::LogarithmicEquation.new(
              normalizer.x,
              normalizer.y,
              [1.0, 0.0],
            )
          end

          it "evaluates at known points" do
            # At x = 1: ln(1) = 0, so y should be close to y_min
            result_at_1 = equation.evaluate(1.0)
            expect(result_at_1).to be_within(5.0).of(0.0)

            # At x = e ≈ 2.718: ln(e) = 1
            result_at_e = equation.evaluate(Math::E)
            expect(result_at_e).to be >= result_at_1 - 0.001

            # At x = 10: ln(10) ≈ 2.3
            result_at_10 = equation.evaluate(10.0)
            expect(result_at_10).to be >= result_at_e - 0.001
          end

          it "maintains logarithmic relationship" do
            # ln(ab) = ln(a) + ln(b), so for y = k*ln(x):
            # y(2*4) should be related to y(2) + y(4)
            y_2 = equation.evaluate(2.0)
            y_4 = equation.evaluate(4.0)
            y_8 = equation.evaluate(8.0)

            # For pure logarithmic: y(8) = y(2) + y(4) + y(1), but we have scaling
            # Just verify the ordering is correct
            expect(y_8).to be >= y_4 - 0.001
            expect(y_4).to be >= y_2 - 0.001
          end
        end
      end

      context "SquareRootEquation" do
        let(:sqrt_records) do
          [
            Stats::DataPoint.new(x: 0, y: 0),
            Stats::DataPoint.new(x: 100, y: 1000),
          ]
        end
        let(:normalizer) { Stats::SquareRootNormalizer.new(sqrt_records) }

        context "with simple coefficients" do
          let(:equation) do
            Stats::SquareRootEquation.new(
              normalizer.x,
              normalizer.y,
              [1.0, 0.0],
            )
          end

          it "evaluates at known points" do
            # At x = 0: sqrt(0) = 0
            result_at_0 = equation.evaluate(0.0)
            expect(result_at_0).to be_within(5.0).of(1000)

            # At x = 1: sqrt(1) = 1
            result_at_1 = equation.evaluate(1.0)
            expect(result_at_1).to be >= result_at_0 - 0.001

            # At x = 4: sqrt(4) = 2
            result_at_4 = equation.evaluate(4.0)
            expect(result_at_4).to be >= result_at_1 - 0.001

            # At x = 100: sqrt(100) = 10
            result_at_100 = equation.evaluate(100.0)
            expect(result_at_100).to be >= result_at_4 - 0.001
          end

          it "maintains square root relationship" do
            # sqrt(x) grows slower than x
            y_4 = equation.evaluate(4.0)
            y_16 = equation.evaluate(16.0)
            y_64 = equation.evaluate(64.0)

            # Verify square root growth pattern
            expect(y_16).to be >= y_4 - 0.001
            expect(y_64).to be >= y_16 - 0.001

            # For pure square root: sqrt(16)/sqrt(4) = 2, sqrt(64)/sqrt(16) = 2
            # The ratio should be similar (accounting for normalization)
            if y_4 >= 0 && y_16 >= 0
              ratio_1 = y_16 / y_4
              ratio_2 = y_64 / y_16

              # Both ratios should be similar for square root function
              expect(ratio_1).to be_within(0.5).of(ratio_2)
            end
          end
        end
      end
    end

    describe "axis bounds impact on evaluation" do
      context "with different axis ranges" do
        let(:narrow_range_records) do
          [
            Stats::DataPoint.new(x: 10, y: 100),
            Stats::DataPoint.new(x: 20, y: 200),
          ]
        end

        let(:wide_range_records) do
          [
            Stats::DataPoint.new(x: 0, y: 0),
            Stats::DataPoint.new(x: 1000, y: 1_000_000),
          ]
        end

        it "produces different results for same coefficients with different axis bounds" do
          narrow_normalizer = Stats::LinearNormalizer.new(narrow_range_records)
          wide_normalizer = Stats::LinearNormalizer.new(wide_range_records)

          # Same coefficients, different normalizers
          equation_narrow =
            Stats::PolynomialEquation.new(
              narrow_normalizer.x,
              narrow_normalizer.y,
              [1.0, 2.0],
            )
          equation_wide =
            Stats::PolynomialEquation.new(
              wide_normalizer.x,
              wide_normalizer.y,
              [1.0, 2.0],
            )

          # Same x value should produce different results due to different normalization
          x_test = 15.0
          result_narrow = equation_narrow.evaluate(x_test)
          result_wide = equation_wide.evaluate(x_test)

          expect(result_narrow).not_to be_within(1.0).of(result_wide)
        end
      end

      context "boundary value testing" do
        let(:normalizer) { Stats::LinearNormalizer.new(sample_records) }
        let(:equation) do
          Stats::PolynomialEquation.new(normalizer.x, normalizer.y, [1.0, 0.01])
        end

        it "handles values at axis boundaries" do
          # Test at exact boundaries
          result_at_min = equation.evaluate(100.0) # x_min
          result_at_max = equation.evaluate(500.0) # x_max

          expect(result_at_min).to be_finite
          expect(result_at_max).to be_finite
          expect(result_at_max).to be > result_at_min
        end

        it "handles values outside axis boundaries" do
          # Test beyond boundaries
          result_below_min = equation.evaluate(50.0) # below x_min
          result_above_max = equation.evaluate(600.0) # above x_max

          expect(result_below_min).to be_finite
          expect(result_above_max).to be_finite
        end
      end
    end

    describe "coefficient impact on evaluation" do
      let(:normalizer) { Stats::LinearNormalizer.new(simple_records) }

      context "with zero coefficients" do
        it "handles all-zero coefficients for polynomial" do
          equation =
            Stats::PolynomialEquation.new(
              normalizer.x,
              normalizer.y,
              [0.0, 0.0, 0.0],
            )
          result = equation.evaluate(5.0)
          expect(result).to be_within(0.001).of(0.0)
        end
      end

      context "with large coefficients" do
        it "handles large polynomial coefficients" do
          equation =
            Stats::PolynomialEquation.new(
              normalizer.x,
              normalizer.y,
              [100.0, 100.0],
            )
          result = equation.evaluate(5.0)
          expect(result).to be_finite
          expect(result).to be > 1000.0
        end
      end

      context "with small coefficients" do
        it "handles small polynomial coefficients" do
          equation =
            Stats::PolynomialEquation.new(
              normalizer.x,
              normalizer.y,
              [1e-6, 1e-6],
            )
          result = equation.evaluate(5.0)
          expect(result).to be_finite
          expect(result).to be > 0
        end
      end
    end

    describe "mathematical consistency" do
      let(:normalizer) { Stats::LinearNormalizer.new(sample_records) }

      context "linear equations" do
        let(:equation) do
          Stats::PolynomialEquation.new(normalizer.x, normalizer.y, [2.0, 3.0])
        end

        it "maintains linear relationship" do
          x1, x2, x3 = 150.0, 250.0, 350.0
          y1 = equation.evaluate(x1)
          y2 = equation.evaluate(x2)
          y3 = equation.evaluate(x3)

          # For linear: (y2-y1)/(x2-x1) = (y3-y2)/(x3-x2)
          slope1 = (y2 - y1) / (x2 - x1)
          slope2 = (y3 - y2) / (x3 - x2)

          expect(slope1).to be_within(0.001).of(slope2)
        end
      end

      context "quadratic equations" do
        let(:equation) do
          Stats::PolynomialEquation.new(
            normalizer.x,
            normalizer.y,
            [1.0, 0.0, 1.0],
          )
        end

        it "maintains quadratic relationship" do
          # For y = x² + 1, second derivative should be constant
          x1, x2, x3, x4 = 200.0, 250.0, 300.0, 350.0
          y1 = equation.evaluate(x1)
          y2 = equation.evaluate(x2)
          y3 = equation.evaluate(x3)
          y4 = equation.evaluate(x4)

          # Calculate second differences
          diff1 = y2 - y1
          diff2 = y3 - y2
          diff3 = y4 - y3

          second_diff1 = diff2 - diff1
          second_diff2 = diff3 - diff2

          # Second differences should be approximately equal for quadratic
          expect(second_diff1).to be_within(0.1).of(second_diff2)
        end
      end
    end
  end

  describe "Stats::PolynomialEquation" do
    let(:normalizer) { Stats::LinearNormalizer.new(sample_records) }

    describe "#initialize" do
      it "initializes with normalizer and coefficients" do
        equation =
          Stats::PolynomialEquation.new(
            normalizer.x,
            normalizer.y,
            [1.0, 2.0, 3.0],
          )
        expect(equation.coefficients).to eq([1.0, 2.0, 3.0])
      end

      it "handles empty coefficients" do
        equation = Stats::PolynomialEquation.new(normalizer.x, normalizer.y, [])
        expect(equation.coefficients).to eq([])
      end
    end

    describe "#evaluate" do
      context "with linear coefficients" do
        let(:equation) do
          Stats::PolynomialEquation.new(
            normalizer.x,
            normalizer.y,
            [100.0, 50.0],
          )
        end

        it "evaluates linear equation correctly" do
          # For a linear equation: y = 100 + 50x
          result = equation.evaluate(200.0)
          expect(result).to be_a(Float)
          expect(result).to be > 0
        end
      end

      context "with quadratic coefficients" do
        let(:equation) do
          Stats::PolynomialEquation.new(
            normalizer.x,
            normalizer.y,
            [1.0, 2.0, 3.0],
          )
        end

        it "evaluates quadratic equation correctly" do
          # For a quadratic equation: y = 1 + 2x + 3x²
          result = equation.evaluate(200.0)
          expect(result).to be_a(Float)
        end
      end

      context "with empty coefficients" do
        let(:equation) do
          Stats::PolynomialEquation.new(normalizer.x, normalizer.y, [])
        end

        it "returns denormalized zero for empty coefficients" do
          result = equation.evaluate(200.0)
          expect(result).to be_a(Float)
        end
      end

      it "handles edge cases" do
        equation =
          Stats::PolynomialEquation.new(normalizer.x, normalizer.y, [0.0])
        result = equation.evaluate(300.0)
        expect(result).to be_a(Float)
      end
    end
  end

  describe "Stats::LogarithmicEquation" do
    let(:normalizer) { Stats::LogarithmicNormalizer.new(sample_records) }

    describe "#evaluate" do
      let(:equation) do
        Stats::LogarithmicEquation.new(normalizer.x, normalizer.y, [2.0, 1.0])
      end

      it "evaluates logarithmic equation correctly" do
        result = equation.evaluate(200.0)
        expect(result).to be_a(Float)
        expect(result).to be > 0
      end

      it "handles minimum x values" do
        result = equation.evaluate(100.0)
        expect(result).to be_a(Float)
      end

      it "handles maximum x values" do
        result = equation.evaluate(500.0)
        expect(result).to be_a(Float)
      end
    end

    describe "#to_s" do
      it "formats logarithmic equation correctly" do
        equation =
          Stats::LogarithmicEquation.new(normalizer.x, normalizer.y, [2.0, 1.0])
        formatted = equation.to_s
        expect(formatted).to include("y = ")
        expect(formatted).to include("ln(x)")
        expect(formatted).to include("*")
        expect(formatted).to include("+")
      end

      it "handles negative coefficients" do
        equation =
          Stats::LogarithmicEquation.new(
            normalizer.x,
            normalizer.y,
            [-2.0, -1.0],
          )
        formatted = equation.to_s
        expect(formatted).to include("y = ")
        expect(formatted).to include("ln(x)")
        expect(formatted).to include("-")
      end

      it "returns formatted equation string" do
        equation =
          Stats::LogarithmicEquation.new(normalizer.x, normalizer.y, [2.0, 1.0])
        expect(equation.to_s).to be_a(String)
        expect(equation.to_s).to include("y = ")
      end
    end
  end

  describe "Stats::SquareRootEquation" do
    let(:normalizer) { Stats::SquareRootNormalizer.new(sample_records) }

    describe "#evaluate" do
      let(:equation) do
        Stats::SquareRootEquation.new(normalizer.x, normalizer.y, [2.0, 1.0])
      end

      it "evaluates square root equation correctly" do
        result = equation.evaluate(200.0)
        expect(result).to be_a(Float)
        expect(result).to be > 0
      end

      it "handles minimum x values" do
        result = equation.evaluate(100.0)
        expect(result).to be_a(Float)
      end

      it "handles maximum x values" do
        result = equation.evaluate(500.0)
        expect(result).to be_a(Float)
      end
    end

    describe "#to_s" do
      it "formats square root equation correctly" do
        equation =
          Stats::SquareRootEquation.new(normalizer.x, normalizer.y, [2.0, 1.0])
        formatted = equation.to_s
        expect(formatted).to include("y = ")
        expect(formatted).to include("√x")
        expect(formatted).to include("*")
        expect(formatted).to include("+")
      end

      it "handles negative coefficients" do
        equation =
          Stats::SquareRootEquation.new(
            normalizer.x,
            normalizer.y,
            [-2.0, -1.0],
          )
        formatted = equation.to_s
        expect(formatted).to include("y = ")
        expect(formatted).to include("√x")
        expect(formatted).to include("-")
      end

      it "returns formatted equation string" do
        equation =
          Stats::SquareRootEquation.new(normalizer.x, normalizer.y, [2.0, 1.0])
        expect(equation.to_s).to be_a(String)
        expect(equation.to_s).to include("y = ")
      end
    end
  end

  describe "Stats::TransformedEquation (abstract base class)" do
    describe "shared behavior" do
      let(:normalizer) { Stats::LogarithmicNormalizer.new(sample_records) }
      let(:equation) do
        Stats::LogarithmicEquation.new(normalizer.x, normalizer.y, [2.0, 1.0])
      end

      it "stores normalized slope and intercept" do
        expect(equation.coefficients).to eq([2.0, 1.0]) # [slope, intercept]
      end

      it "evaluates using the normalized transformation" do
        result = equation.evaluate(200.0)
        expect(result).to be_a(Float)
      end
    end
  end

  describe "Edge cases and error handling" do
    let(:normalizer) { Stats::LinearNormalizer.new(sample_records) }

    it "handles extreme coefficient values" do
      equation =
        Stats::PolynomialEquation.new(normalizer.x, normalizer.y, [1e10, -1e10])
      result = equation.evaluate(200.0)
      expect(result).to be_a(Float)
    end

    it "handles very small coefficient values" do
      equation =
        Stats::PolynomialEquation.new(
          normalizer.x,
          normalizer.y,
          [1e-10, 1e-10],
        )
      result = equation.evaluate(200.0)
      expect(result).to be_a(Float)
    end

    it "handles single coefficient polynomial" do
      equation =
        Stats::PolynomialEquation.new(normalizer.x, normalizer.y, [42.0])
      result = equation.evaluate(200.0)
      expect(result).to be_a(Float)
    end
  end

  describe "Integration with normalizers" do
    let(:linear_normalizer) { Stats::LinearNormalizer.new(sample_records) }
    let(:quadratic_normalizer) do
      Stats::QuadraticNormalizer.new(sample_records)
    end
    let(:log_normalizer) { Stats::LogarithmicNormalizer.new(sample_records) }
    let(:sqrt_normalizer) { Stats::SquareRootNormalizer.new(sample_records) }

    it "works with different normalizer types" do
      equations = [
        Stats::PolynomialEquation.new(
          linear_normalizer.x,
          linear_normalizer.y,
          [1.0, 2.0],
        ),
        Stats::PolynomialEquation.new(
          quadratic_normalizer.x,
          quadratic_normalizer.y,
          [1.0, 2.0, 3.0],
        ),
        Stats::LogarithmicEquation.new(
          log_normalizer.x,
          log_normalizer.y,
          [2.0, 1.0],
        ),
        Stats::SquareRootEquation.new(
          sqrt_normalizer.x,
          sqrt_normalizer.y,
          [2.0, 1.0],
        ),
      ]

      equations.each do |equation|
        result = equation.evaluate(300.0)
        expect(result).to be_a(Float)
        expect(equation.to_s).to include("y = ")
      end
    end

    it "handles the full range of input values" do
      equation =
        Stats::PolynomialEquation.new(
          linear_normalizer.x,
          linear_normalizer.y,
          [1.0, 2.0],
        )

      sample_records.each do |record|
        result = equation.evaluate(record.x)
        expect(result).to be_a(Float)
        expect(result).to be_finite
      end
    end
  end
end
