local tracker = require 'u.tracker'
local Signal = tracker.Signal
local ExecutionContext = tracker.ExecutionContext

describe('Signal', function()
  local signal

  before_each(function() signal = Signal:new(0, 'testSignal') end)

  it('should initialize with correct parameters', function()
    assert.is.equal(signal.value, 0)
    assert.is.equal(signal.name, 'testSignal')
    assert.is.not_nil(signal.subscribers)
    assert.is.equal(#signal.subscribers, 0)
    assert.is.equal(signal.changing, false)
  end)

  it('should set new value and notify subscribers', function()
    local called = false
    signal:subscribe(function(value)
      called = true
      assert.is.equal(value, 42)
    end)

    signal:set(42)
    assert.is.equal(called, true)
  end)

  it('should not notify subscribers during circular dependency', function()
    signal.changing = true
    local notified = false

    signal:subscribe(function() notified = true end)

    signal:set(42)
    assert.is.equal(notified, false) -- No notification should occur
  end)

  it('should get current value', function()
    signal:set(100)
    assert.is.equal(signal:get(), 100)
  end)

  it('should update value with function', function()
    signal:set(10)
    signal:update(function(value) return value * 2 end)
    assert.is.equal(signal:get(), 20)
  end)

  it('should dispose subscribers', function()
    local called = false
    local unsubscribe = signal:subscribe(function() called = true end)

    unsubscribe()
    signal:set(10)
    assert.is.equal(called, false) -- Should not be notified
  end)

  describe('Signal:map', function()
    it('should transform the signal value', function()
      local signal = Signal:new(5)
      local mapped_signal = signal:map(function(value) return value * 2 end)

      assert.is.equal(mapped_signal:get(), 10) -- Initial transformation
      signal:set(10)
      assert.is.equal(mapped_signal:get(), 20) -- Updated transformation
    end)

    it('should handle empty transformations', function()
      local signal = Signal:new(nil)
      local mapped_signal = signal:map(function(value) return value or 'default' end)

      assert.is.equal(mapped_signal:get(), 'default') -- Return default
      signal:set 'new value'
      assert.is.equal(mapped_signal:get(), 'new value') -- Return new value
    end)
  end)

  describe('Signal:filter', function()
    it('should only emit values that pass the filter', function()
      local signal = Signal:new(5)
      local filtered_signal = signal:filter(function(value) return value > 10 end)

      assert.is.equal(filtered_signal:get(), nil) -- Initial value should not pass
      signal:set(15)
      assert.is.equal(filtered_signal:get(), 15) -- Now filtered
      signal:set(8)
      assert.is.equal(filtered_signal:get(), 15) -- Does not pass the filter
    end)

    it('should handle empty initial values', function()
      local signal = Signal:new(nil)
      local filtered_signal = signal:filter(function(value) return value ~= nil end)

      assert.is.equal(filtered_signal:get(), nil) -- Should be nil
      signal:set(10)
      assert.is.equal(filtered_signal:get(), 10) -- Should pass now
    end)
  end)

  describe('create_memo', function()
    it('should compute a derived value and update when dependencies change', function()
      local signal = Signal:new(2)
      local memoized_signal = tracker.create_memo(function() return signal:get() * 2 end)

      assert.is.equal(memoized_signal:get(), 4) -- Initially compute 2 * 2

      signal:set(3)
      assert.is.equal(memoized_signal:get(), 6) -- Update to 3 * 2 = 6

      signal:set(5)
      assert.is.equal(memoized_signal:get(), 10) -- Update to 5 * 2 = 10
    end)

    it('should not recompute if the dependencies do not change', function()
      local call_count = 0
      local signal = Signal:new(10)
      local memoized_signal = tracker.create_memo(function()
        call_count = call_count + 1
        return signal:get() + 1
      end)

      assert.is.equal(memoized_signal:get(), 11) -- Compute first value
      assert.is.equal(call_count, 1) -- Should compute once

      memoized_signal:get() -- Call again, should use memoized value
      assert.is.equal(call_count, 1) -- Still should only be one call

      signal:set(10) -- Set the same value
      assert.is.equal(memoized_signal:get(), 11)
      assert.is.equal(call_count, 2)

      signal:set(20)
      assert.is.equal(memoized_signal:get(), 21)
      assert.is.equal(call_count, 3)
    end)
  end)

  describe('create_effect', function()
    it('should track changes and execute callback', function()
      local signal = Signal:new(5)
      local call_count = 0

      tracker.create_effect(function()
        signal:get() -- track as a dependency
        call_count = call_count + 1
      end)

      assert.is.equal(call_count, 1)
      signal:set(10)
      assert.is.equal(call_count, 2)
    end)

    it('should clean up signals and not call after dispose', function()
      local signal = Signal:new(5)
      local call_count = 0

      local unsubscribe = tracker.create_effect(function()
        call_count = call_count + 1
        return signal:get() * 2
      end)

      assert.is.equal(call_count, 1) -- Initially calls
      unsubscribe() -- Unsubscribe the effect
      signal:set(10) -- Update signal value
      assert.is.equal(call_count, 1) -- Callback should not be called again
    end)
  end)
end)

describe('ExecutionContext', function()
  local context

  before_each(function() context = ExecutionContext:new() end)

  it('should initialize a new context', function()
    assert.is.table(context.signals)
    assert.is.table(context.subscribers)
  end)

  it('should track signals', function()
    local signal = Signal:new(0)
    context:track(signal)

    assert.is.equal(next(context.signals), signal) -- Check if signal is tracked
  end)

  it('should subscribe to signals', function()
    local signal = Signal:new(0)
    local callback_called = false

    context:track(signal)
    context:subscribe(function() callback_called = true end)

    signal:set(100)
    assert.is.equal(callback_called, true) -- Callback should be called
  end)

  it('should dispose tracked signals', function()
    local signal = Signal:new(0)
    context:track(signal)

    context:dispose()
    assert.is.falsy(next(context.signals)) -- Should not have any tracked signals
  end)

  it('should run callbacks in the correct context', function()
    local signal = Signal:new(0)
    local callback_called = false

    context:track(signal)
    context:run(function()
      context:subscribe(function() callback_called = true end)
      signal:set(1)
    end)

    assert.is.equal(callback_called, true) -- Callback should be called
  end)
end)