-- Copyright (c) Microsoft Corporation.
-- Licensed under the PostgreSQL License.

-- Merged from: 08_loop_cancel, 24_loop_break
-- Tests: loop execution and cancellation, loop break via df.break(), while-loop condition
SET SESSION AUTHORIZATION df_e2e_user;

-- === Test: 08_loop_cancel ===

DROP TABLE IF EXISTS test_loop_log;
CREATE TABLE test_loop_log (id SERIAL, iteration INT, variant TEXT, ts TIMESTAMP DEFAULT now());

CREATE TEMP TABLE _test_state (instance_id TEXT, variant TEXT);

-- Variant A: df.loop() function
INSERT INTO _test_state SELECT df.start(
    df.loop(
        'INSERT INTO test_loop_log (iteration, variant) VALUES ((SELECT COALESCE(MAX(iteration), 0) + 1 FROM test_loop_log WHERE variant = ''func''), ''func'')'
        ~> df.sleep(1)
    ),
    'test-loop-func'
), 'func';

-- Variant B: @> operator
INSERT INTO _test_state SELECT df.start(
    @> (
        'INSERT INTO test_loop_log (iteration, variant) VALUES ((SELECT COALESCE(MAX(iteration), 0) + 1 FROM test_loop_log WHERE variant = ''op''), ''op'')'
        ~> df.sleep(1)
    ),
    'test-loop-op'
), 'op';

DO $$
DECLARE
    rec RECORD;
    status TEXT;
    cnt INT;
    attempts INT;
BEGIN
    FOR rec IN SELECT instance_id, variant FROM _test_state LOOP
        RAISE NOTICE 'Testing % variant: %', rec.variant, rec.instance_id;
        attempts := 0;
        
        -- Wait for at least 2 iterations
        LOOP
            SELECT COUNT(*) INTO cnt FROM test_loop_log WHERE variant = rec.variant;
            EXIT WHEN cnt >= 2 OR attempts > 100;
            PERFORM pg_sleep(0.5);
            attempts := attempts + 1;
        END LOOP;
        
        IF cnt < 2 THEN
            RAISE EXCEPTION 'TEST FAILED [%]: expected at least 2 iterations, got %', rec.variant, cnt;
        END IF;
        
        -- Cancel the loop
        PERFORM df.cancel(rec.instance_id, 'Test complete');
        
        -- df.cancel immediately marks df.instances.status = 'cancelled'.
        -- Poll until df.status() reflects that (should be instant, but allow a short window
        -- in case an in-flight update_instance_status activity hasn't been guarded yet).
        attempts := 0;
        LOOP
            SELECT s INTO status FROM df.status(rec.instance_id) s;
            EXIT WHEN lower(status) = 'cancelled' OR attempts > 100;
            PERFORM pg_sleep(0.2);
            attempts := attempts + 1;
        END LOOP;
        
        IF lower(status) != 'cancelled' THEN
            RAISE EXCEPTION 'TEST FAILED [%]: expected cancelled, got %', rec.variant, status;
        END IF;
        
        RAISE NOTICE 'PASSED: loop_cancel [%]', rec.variant;
    END LOOP;
    
    RAISE NOTICE 'TEST PASSED: loop_cancel (func + @> operator)';
END $$;

DROP TABLE _test_state;
DROP TABLE test_loop_log;

-- === Test: 24_loop_break ===

DROP TABLE IF EXISTS test_break_log;
CREATE TABLE test_break_log (id SERIAL, iteration INT, test_name TEXT, ts TIMESTAMP DEFAULT now());

-- Test 1: df.break() exits the loop
CREATE TEMP TABLE _test1_state AS
SELECT df.start(
    df.loop(
        'INSERT INTO test_break_log (iteration, test_name) VALUES ((SELECT COALESCE(MAX(iteration), 0) + 1 FROM test_break_log WHERE test_name = ''break_test''), ''break_test'')'
        ~> (
            'SELECT COUNT(*) >= 3 FROM test_break_log WHERE test_name = ''break_test'''
                ?> df.break('{"reason": "reached 3 iterations"}')
                !> df.sleep(1)
        )
    ),
    'test-break'
) AS instance_id;

DO $$
DECLARE
    v_instance_id TEXT;
    v_status TEXT;
    v_cnt INT;
BEGIN
    SELECT instance_id INTO v_instance_id FROM _test1_state;
    RAISE NOTICE 'Test 1 - df.break(): instance %', v_instance_id;
    
    SELECT df.await_instance(v_instance_id, 50) INTO v_status;
    
    SELECT COUNT(*) INTO v_cnt FROM test_break_log WHERE test_name = 'break_test';
    
    IF v_status != 'completed' THEN
        RAISE EXCEPTION 'TEST FAILED [break]: expected Completed, got %', v_status;
    END IF;
    
    IF v_cnt != 3 THEN
        RAISE EXCEPTION 'TEST FAILED [break]: expected 3 iterations, got %', v_cnt;
    END IF;
    
    RAISE NOTICE 'PASSED: df.break() - loop exited after 3 iterations';
END $$;

DROP TABLE _test1_state;

-- Test 2: df.loop(body, condition) - while loop
CREATE TEMP TABLE _test2_state AS
SELECT df.start(
    df.loop(
        'INSERT INTO test_break_log (iteration, test_name) VALUES ((SELECT COALESCE(MAX(iteration), 0) + 1 FROM test_break_log WHERE test_name = ''while_test''), ''while_test'')'
        ~> df.sleep(1),
        'SELECT COUNT(*) < 4 FROM test_break_log WHERE test_name = ''while_test'''
    ),
    'test-while-loop'
) AS instance_id;

DO $$
DECLARE
    v_instance_id TEXT;
    v_status TEXT;
    v_cnt INT;
BEGIN
    SELECT instance_id INTO v_instance_id FROM _test2_state;
    RAISE NOTICE 'Test 2 - while loop: instance %', v_instance_id;
    
    SELECT df.await_instance(v_instance_id, 50) INTO v_status;
    
    SELECT COUNT(*) INTO v_cnt FROM test_break_log WHERE test_name = 'while_test';
    
    IF v_status != 'completed' THEN
        RAISE EXCEPTION 'TEST FAILED [while]: expected Completed, got %', v_status;
    END IF;
    
    IF v_cnt != 4 THEN
        RAISE EXCEPTION 'TEST FAILED [while]: expected 4 iterations, got %', v_cnt;
    END IF;
    
    RAISE NOTICE 'PASSED: while loop - exited when condition became false';
END $$;

DROP TABLE _test2_state;

-- Test 3: df.break() with return value
CREATE TEMP TABLE _test3_state AS
SELECT df.start(
    df.loop(
        'INSERT INTO test_break_log (iteration, test_name) VALUES ((SELECT COALESCE(MAX(iteration), 0) + 1 FROM test_break_log WHERE test_name = ''break_value_test''), ''break_value_test'')'
        ~> (
            'SELECT COUNT(*) >= 2 FROM test_break_log WHERE test_name = ''break_value_test'''
                ?> df.break('{"total": 2, "status": "done"}')
                !> df.sleep(1)
        )
    ),
    'test-break-value'
) AS instance_id;

DO $$
DECLARE
    v_instance_id TEXT;
    v_status TEXT;
    v_result TEXT;
BEGIN
    SELECT instance_id INTO v_instance_id FROM _test3_state;
    RAISE NOTICE 'Test 3 - df.break(value): instance %', v_instance_id;
    
    SELECT df.await_instance(v_instance_id, 50) INTO v_status;
    
    IF v_status != 'completed' THEN
        RAISE EXCEPTION 'TEST FAILED [break-value]: expected Completed, got %', v_status;
    END IF;
    
    SELECT r INTO v_result FROM df.result(v_instance_id) r;
    RAISE NOTICE 'Break returned: %', v_result;
    
    IF v_result IS NULL OR v_result::jsonb->>'status' IS NULL THEN
        RAISE NOTICE 'Note: Break value not directly accessible in df.result() - this is expected for now';
    END IF;
    
    RAISE NOTICE 'PASSED: df.break(value) - loop completed with value';
END $$;

DROP TABLE _test3_state;
DROP TABLE test_break_log;

-- Test 4: named result on the LOOP node itself is accessible downstream
DROP TABLE IF EXISTS test_loop_named;
CREATE TABLE test_loop_named (id SERIAL, status TEXT);

CREATE TEMP TABLE _test4_state AS
SELECT df.start(
    (df.loop(df.break('{"status": "done"}')) |=> 'loop_result')
    ~> $$INSERT INTO test_loop_named (status) VALUES ($loop_result::jsonb->>'status')$$,
    'test-loop-named-result'
) AS instance_id;

DO $$
DECLARE
    v_instance_id TEXT;
    v_status TEXT;
    v_loop_status TEXT;
BEGIN
    SELECT instance_id INTO v_instance_id FROM _test4_state;
    RAISE NOTICE 'Test 4 - named loop result: instance %', v_instance_id;

    SELECT df.await_instance(v_instance_id, 20) INTO v_status;

    IF v_status != 'completed' THEN
        RAISE EXCEPTION 'TEST FAILED [loop-named]: expected Completed, got %', v_status;
    END IF;

    SELECT status INTO v_loop_status FROM test_loop_named ORDER BY id DESC LIMIT 1;

    IF v_loop_status != 'done' THEN
        RAISE EXCEPTION 'TEST FAILED [loop-named]: expected done, got %', v_loop_status;
    END IF;

    RAISE NOTICE 'PASSED: named LOOP result stored';
END $$;

DROP TABLE _test4_state;
DROP TABLE test_loop_named;

-- === Test: running_status_during_loop ===
-- Verify that df.status() reports 'running' while a loop is actively executing
-- (regression test for: loops reporting 'pending' instead of 'running')

DROP TABLE IF EXISTS test_running_status_log;
CREATE TABLE test_running_status_log (id SERIAL, ts TIMESTAMP DEFAULT now());

CREATE TEMP TABLE _test_running_state AS
SELECT df.start(
    df.loop(
        'INSERT INTO test_running_status_log DEFAULT VALUES'
        ~> df.sleep(1)
    ),
    'test-running-status'
) AS instance_id;

DO $$
DECLARE
    v_instance_id TEXT;
    v_status TEXT;
    v_cnt INT;
    attempts INT := 0;
BEGIN
    SELECT instance_id INTO v_instance_id FROM _test_running_state;
    RAISE NOTICE 'Test running_status_during_loop: instance %', v_instance_id;

    -- Wait until at least one iteration has completed so the worker has
    -- clearly started executing the loop body.
    LOOP
        SELECT COUNT(*) INTO v_cnt FROM test_running_status_log;
        EXIT WHEN v_cnt >= 1 OR attempts > 100;
        PERFORM pg_sleep(0.2);
        attempts := attempts + 1;
    END LOOP;

    IF v_cnt < 1 THEN
        RAISE EXCEPTION 'TEST FAILED [running_status]: loop body never executed';
    END IF;

    -- The instance must now report 'running', not 'pending'
    SELECT s INTO v_status FROM df.status(v_instance_id) s;

    IF lower(v_status) != 'running' THEN
        RAISE EXCEPTION 'TEST FAILED [running_status]: expected running, got %', v_status;
    END IF;

    RAISE NOTICE 'PASSED: running_status_during_loop - status is running while loop executes';

    -- Cancel to clean up
    PERFORM df.cancel(v_instance_id, 'Test complete');
END $$;

DROP TABLE _test_running_state;
DROP TABLE test_running_status_log;

-- === Test: zero_sleep_loop_rate_limited ===
-- Regression test for: df.loop(df.sleep(0)) busy-spin (issue #13).
-- A loop whose body contains only a zero-duration sleep must NOT spin at full
-- CPU speed.  With the 1-second minimum-iteration delay enforced by the loop
-- handler, at most a handful of iterations should complete in 3 seconds.

DROP TABLE IF EXISTS test_zero_sleep_log;
CREATE TABLE test_zero_sleep_log (id SERIAL, ts TIMESTAMP DEFAULT now());

CREATE TEMP TABLE _test_zero_sleep_state AS
SELECT df.start(
    df.loop(
        'INSERT INTO test_zero_sleep_log DEFAULT VALUES'
        ~> df.sleep(0)
    ),
    'test-loop-zero-sleep'
) AS instance_id;

DO $$
DECLARE
    v_instance_id TEXT;
    v_status      TEXT;
    v_cnt         INT;
    attempts      INT := 0;
BEGIN
    SELECT instance_id INTO v_instance_id FROM _test_zero_sleep_state;
    RAISE NOTICE 'Test zero_sleep_loop_rate_limited: instance %', v_instance_id;

    -- Wait until at least 1 iteration has run so the loop is clearly started.
    LOOP
        SELECT COUNT(*) INTO v_cnt FROM test_zero_sleep_log;
        EXIT WHEN v_cnt >= 1 OR attempts > 100;
        PERFORM pg_sleep(0.1);
        attempts := attempts + 1;
    END LOOP;

    IF v_cnt < 1 THEN
        RAISE EXCEPTION 'TEST FAILED [zero-sleep]: loop body never executed';
    END IF;

    -- Confirm the loop is still running (not failed/errored after the first iteration).
    SELECT s INTO v_status FROM df.status(v_instance_id) s;
    IF lower(v_status) != 'running' THEN
        RAISE EXCEPTION 'TEST FAILED [zero-sleep]: expected running before observation window, got %', v_status;
    END IF;

    -- Let it run for ~3 more seconds and count iterations.
    PERFORM pg_sleep(3);
    SELECT COUNT(*) INTO v_cnt FROM test_zero_sleep_log;

    -- Lower bound: the loop must have made meaningful progress.
    IF v_cnt < 2 THEN
        RAISE EXCEPTION 'TEST FAILED [zero-sleep]: only % iterations in ~3s; rate-limit may be too aggressive', v_cnt;
    END IF;

    -- With a 1-second minimum delay per continue_as_new, the loop cannot
    -- complete more than ~4 iterations in 3 seconds (generous upper bound of
    -- 15 to accommodate slow CI environments).  Without the fix it would run
    -- hundreds of times in the same window.
    IF v_cnt > 15 THEN
        RAISE EXCEPTION 'TEST FAILED [zero-sleep]: % iterations in ~3s (expected <= 15); minimum rate-limit may not be working', v_cnt;
    END IF;

    RAISE NOTICE 'PASSED: zero_sleep_loop_rate_limited - % iterations in ~3s (within expected range)', v_cnt;

    PERFORM df.cancel(v_instance_id, 'Test complete');
END $$;

DROP TABLE _test_zero_sleep_state;
DROP TABLE test_zero_sleep_log;

RESET SESSION AUTHORIZATION;
SELECT 'TEST PASSED' AS result;
