using Jint.Runtime.Debugger;

namespace Jint.Tests.Runtime.Debugger
{
    public class StepModeTests
    {
        /// <summary>
        /// Helper method to keep tests independent of line numbers, columns or other arbitrary assertions on
        /// the current statement. Steps through script with StepMode.Into until it reaches literal statement
        /// (or directive) 'source'. Then counts the steps needed to reach 'target' using the indicated StepMode.
        /// </summary>
        /// <param name="script">Script used as basis for test</param>
        /// <param name="stepMode">StepMode to use from source to target</param>
        /// <returns>Number of steps from source to target</returns>
        private static int StepsFromSourceToTarget(string script, StepMode stepMode)
        {
            var engine = new Engine(options => options
                .DebugMode()
                .InitialStepMode(StepMode.Into)
                .DebuggerStatementHandling(DebuggerStatementHandling.Script));

            int steps = 0;
            bool sourceReached = false;
            bool targetReached = false;
            engine.Debugger.Step += (sender, info) =>
            {
                if (sourceReached)
                {
                    steps++;
                    if (info.ReachedLiteral("target"))
                    {
                        // Stop stepping
                        targetReached = true;
                        return StepMode.None;
                    }
                    return stepMode;
                }
                else if (info.ReachedLiteral("source"))
                {
                    sourceReached = true;
                    return stepMode;
                }
                return StepMode.Into;
            };

            engine.Execute(script);
            
            // Make sure we actually reached the target
            Assert.True(targetReached);

            return steps;
        }

        [Fact]
        public void StepsIntoRegularFunctionCall()
        {
            var script = @"
                'source';
                test(); // first step
                function test()
                {
                    'target'; // second step
                }";

            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into));
        }

        [Fact]
        public void StepsOverRegularFunctionCall()
        {
            var script = @"
                'source';
                test();
                'target';
                function test()
                {
                    'dummy';
                }";

            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over));
        }

        [Fact]
        public void StepsOutOfRegularFunctionCall()
        {
            var script = @"
                test();
                'target';

                function test()
                {
                    'source';
                    'dummy';
                }";

            Assert.Equal(1, StepsFromSourceToTarget(script, StepMode.Out));
        }

        [Fact]
        public void StepsIntoMemberFunctionCall()
        {
            var script = @"
                const obj = {
                    test()
                    {
                        'target'; // second step
                    }
                };
                'source';
                obj.test(); // first step";

            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into));
        }

        [Fact]
        public void StepsOverMemberFunctionCall()
        {
            var script = @"
                const obj = {
                    test()
                    {
                        'dummy';
                    }
                };
                'source';
                obj.test();
                'target';";

            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over));
        }

        [Fact]
        public void StepsOutOfMemberFunctionCall()
        {
            var script = @"
                const obj = {
                    test()
                    {
                        'source';
                        'dummy';
                    }
                };
                obj.test();
                'target';";

            Assert.Equal(1, StepsFromSourceToTarget(script, StepMode.Out));
        }

        [Fact]
        public void StepsIntoCallExpression()
        {
            var script = @"
                function test()
                {
                    'target'; // second step
                    return 42;
                }
                'source';
                const x = test(); // first step";

            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into));
        }

        [Fact]
        public void StepsOverCallExpression()
        {
            var script = @"
                function test()
                {
                    'dummy';
                    return 42;
                }
                'source';
                const x = test();
                'target';";

            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over));
        }

        [Fact]
        public void StepsOutOfCallExpression()
        {
            var script = @"
                function test()
                {
                    'source';
                    'dummy';
                    return 42;
                }
                const x = test();
                'target';";

            Assert.Equal(1, StepsFromSourceToTarget(script, StepMode.Out));
        }

        [Fact]
        public void StepsIntoGetAccessor()
        {
            var script = @"
                const obj = {
                    get test()
                    {
                        'target'; // second step
                        return 144;
                    }
                };
                'source';
                const x = obj.test; // first step";

            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into));
        }

        [Fact]
        public void StepsOverGetAccessor()
        {
            var script = @"
                const obj = {
                    get test()
                    {
                        return 144;
                    }
                };
                'source';
                const x = obj.test;
                'target';";

            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over));
        }

        [Fact]
        public void StepsOutOfGetAccessor()
        {
            var script = @"
                const obj = {
                    get test()
                    {
                        'source';
                        'dummy';
                        return 144;
                    }
                };
                const x = obj.test;
                'target';";

            Assert.Equal(1, StepsFromSourceToTarget(script, StepMode.Out));
        }

        [Fact]
        public void StepsIntoSetAccessor()
        {
            var script = @"
                const obj = {
                    set test(value)
                    {
                        'target'; // second step
                        this.value = value;
                    }
                };
                'source';
                obj.test = 37; // first step";

            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into));
        }

        [Fact]
        public void StepsOverSetAccessor()
        {
            var script = @"
                const obj = {
                    set test(value)
                    {
                        this.value = value;
                    }
                };
                'source';
                obj.test = 37;
                'target';";

            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over));
        }

        [Fact]
        public void StepsOutOfSetAccessor()
        {
            var script = @"
                const obj = {
                    set test(value)
                    {
                        'source';
                        'dummy';
                        this.value = value;
                    }
                };
                obj.test = 37;
                'target';";

            Assert.Equal(1, StepsFromSourceToTarget(script, StepMode.Out));
        }

        [Fact]
        public void ReturnPointIsAStep()
        {
            var script = @"
                function test()
                {
                    'source';
                }
                test();
                'target';";
            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over));
        }

        [Fact]
        public void ReturnStatementIsAStep()
        {
            var script = @"
                function test()
                {
                    'source';
                    return 'result';
                }
                test();
                'target';";
            Assert.Equal(3, StepsFromSourceToTarget(script, StepMode.Over));
        }

        [Fact]
        public void StepOutOnlyStepsOutOneStackLevel()
        {
            var script = @"
                function test()
                {
                    'dummy';
                    test2();
                    'target';
                }

                function test2()
                {
                    'source';
                    'dummy';
                    'dummy';
                }

                test();";

            var engine = new Engine(options => options.DebugMode());
            int step = 0;
            engine.Debugger.Step += (sender, info) =>
            {
                switch (step)
                {
                    case 0:
                        if (info.ReachedLiteral("source"))
                        {
                            step++;
                            return StepMode.Out;
                        }
                        break;
                    case 1:
                        Assert.True(info.ReachedLiteral("target"));
                        step++;
                        break;
                }
                return StepMode.Into;
            };

            engine.Execute(script);
        }

        [Fact]
        public void StepOverDoesSinglestepAfterBreakpoint()
        {
            string script = @"
                test();

                function test()
                {
                    'dummy';
                    debugger;
                    'target';
                }";

            var engine = new Engine(options => options
                .DebugMode()
                .DebuggerStatementHandling(DebuggerStatementHandling.Script));

            bool stepping = false;

            engine.Debugger.Break += (sender, info) =>
            {
                stepping = true;
                return StepMode.Over;
            };
            engine.Debugger.Step += (sender, info) =>
            {
                if (stepping)
                {
                    Assert.True(info.ReachedLiteral("target"));
                }
                return StepMode.None;
            };

            engine.Execute(script);
        }

        [Fact]
        public void StepNotTriggeredWhenRunning()
        {
            string script = @"
                test();

                function test()
                {
                    'dummy';
                    'dummy';
                }";

            var engine = new Engine(options => options
                .DebugMode()
                .InitialStepMode(StepMode.Into));

            int stepCount = 0;
            engine.Debugger.Step += (sender, info) =>
            {
                stepCount++;
                // Start running after first step
                return StepMode.None;
            };

            engine.Execute(script);

            Assert.Equal(1, stepCount);
        }

        [Fact]
        public void SkipIsTriggeredWhenRunning()
        {
            string script = @"
                'step';
                'skip';
                'skip';
                debugger;
                'step';
                'step';
                ";

            var engine = new Engine(options => options
                .DebugMode()
                .DebuggerStatementHandling(DebuggerStatementHandling.Script)
                .InitialStepMode(StepMode.Into));

            int stepCount = 0;
            int skipCount = 0;

            engine.Debugger.Step += (sender, info) =>
            {
                Assert.True(TestHelpers.IsLiteral(info.CurrentNode, "step"));
                stepCount++;
                // Start running after first step
                return stepCount == 1 ? StepMode.None : StepMode.Into;
            };

            engine.Debugger.Skip += (sender, info) =>
            {
                Assert.True(TestHelpers.IsLiteral(info.CurrentNode, "skip"));
                skipCount++;
                return StepMode.None;
            };

            engine.Debugger.Break += (sender, info) =>
            {
                // Back to stepping after debugger statement
                return StepMode.Into;
            };

            engine.Execute(script);

            Assert.Equal(2, skipCount);
            Assert.Equal(3, stepCount);
        }
    }
}
