use strict;
use warnings;
use Digest::MD5 qw(md5_hex);
use File::Temp qw(tempdir);
use Test::More;
use t::Util;

plan skip_all => 'mruby support is off'
    unless server_features()->{mruby};

plan skip_all => 'curl not found'
    unless prog_exists('curl');

subtest "handler-file" => sub {
    my $server = spawn_h2o(<< 'EOT');
hosts:
  default:
    paths:
      /:
        mruby.handler-file: t/50mruby/hello.rb
EOT
    my ($headers, $body) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    is $body, "hello from h2o_mruby\n";
    like $headers, qr{^HTTP/1\.1 200 OK\r\n}s;
    like $headers, qr{^content-type: text/plain; charset=utf-8\r$}im;
};

subtest "basic" => sub {
    my $server = spawn_h2o(<< 'EOT');
hosts:
  default:
    paths:
      /inline:
        mruby.handler: |
          Proc.new do |env|
            [200, {}, ["hello from h2o_mruby\n"]]
          end
      /return-404:
        mruby.handler: |
          Proc.new do |env|
            [404, {}, ["not found"]]
          end
        file.dir: examples/doc_root
      /fallthru:
        mruby.handler: |
          Proc.new do |env|
            [399, {}, []]
          end
        file.dir: t/50mruby/
      /echo:
        mruby.handler: |
          Proc.new do |env|
            [200, {}, [JSON.generate(env)]]
          end
      /headers:
        mruby.handler: |
          Proc.new do |env|
            [200, {"foo" => "123\n456", "bar" => "baz"}, []]
          end
      /headers-each:
        mruby.handler: |
          Proc.new do |env|
            [200, [["content-type", "text/plain"], ["hello", "world"]], []]
          end
EOT
    my $fetch = sub {
        my $path = shift;
        run_prog("curl --silent -A h2o_mruby_test --dump-header /dev/stderr http://127.0.0.1:$server->{port}$path");
    };
    my ($headers, $body) = $fetch->("/inline/");
    is $body, "hello from h2o_mruby\n", "inline";
    subtest "return-404" => sub {
        ($headers, $body) = $fetch->("/return-404/");
        like $headers, qr{^HTTP/1\.1 404 }is;
        is $body, "not found";
    };
    subtest "fallthru" => sub {
        ($headers, $body) = $fetch->("/fallthru/");
        like $headers, qr{^HTTP/1\.1 200 OK\r\n}is;
        is md5_hex($body), md5_file("t/50mruby/index.html");
        my @dates = $headers =~ /^date: .+?\r$/img;
        is scalar(@dates), 1, 'duplicate date header';
    };
    subtest "echo" => sub {
        ($headers, $body) = $fetch->("/echo/abc?def");
        like $body, qr{"REQUEST_METHOD":"GET"}, "REQUEST_METHOD";
        like $body, qr{"SCRIPT_NAME":"/echo"}, "SCRIPT_NAME";
        like $body, qr{"PATH_INFO":"/abc"}, "PATH_INFO";
        like $body, qr{"QUERY_STRING":"def"}, "QUERY_STRING";
        like $body, qr{"SERVER_NAME":"default"}, "SERVER_NAME";
        like $body, qr{"SERVER_ADDR":"127.0.0.1"}, "SERVER_ADDR";
        like $body, qr{"SERVER_PORT":"$server->{port}"}, "SERVER_PORT";
        like $body, qr{"HTTP_HOST":"127.0.0.1:$server->{port}"}, "HTTP_HOST";
        like $body, qr{"SERVER_ADDR":"127.0.0.1"}, "REMOTE_ADDR";
        like $body, qr{"SERVER_PORT":"[0-9]+"}, "REMOTE_PORT";
        like $body, qr{"HTTP_USER_AGENT":"h2o_mruby_test"}, "HTTP_USER_AGENT";
        like $body, qr{"rack.url_scheme":"http"}, "url_scheme";
        like $body, qr{"SERVER_SOFTWARE":"h2o/[0-9]+\.[0-9]+\.[0-9]+}, "SERVER_SOFTWARE";
    };
    subtest "protocol" => sub {
        run_with_curl($server, sub {
                my ($proto, $port, $curl) = @_;
                my $content = `$curl --silent --show-error $proto://127.0.0.1:$port/echo`;
                if ($curl =~ /--http2/) {
                    like $content, qr{"SERVER_PROTOCOL":"HTTP/2"}, "SERVER_PROTOCOL";
                } elsif ($curl =~ /--http3/) {
                    like $content, qr{"SERVER_PROTOCOL":"HTTP/3"}, "SERVER_PROTOCOL";
                } else {
                    like $content, qr{"SERVER_PROTOCOL":"HTTP/1\.1"}, "SERVER_PROTOCOL";
                }
            });
    };
    subtest "headers" => sub {
        ($headers, $body) = $fetch->("/headers/");
        like $headers, qr{^foo: 123\r$}mi;
        like $headers, qr{^foo: 456\r$}mi;
        like $headers, qr{^bar: baz\r$}mi;
    };
    subtest "headers-each" => sub {
        ($headers, $body) = $fetch->("/headers-each/");
        like $headers, qr{^content-type: text/plain\r$}mi;
        like $headers, qr{^hello: world\r$}mi;
    };
};

subtest "reprocess_request" => sub {
    my $server = spawn_h2o(<< "EOT");
hosts:
  default:
    reproxy: ON
    paths:
      /:
        mruby.handler: |
          Proc.new do |env|
            [200, {"x-reproxy-url" => "http://default/dest#{env["PATH_INFO"]}"}, ["should never see this"]]
          end
      /307:
        mruby.handler: |
          Proc.new do |env|
            [307, {"x-reproxy-url" => "http://default/dest#{env["PATH_INFO"]}"}, ["should never see this"]]
          end
      /dest:
        mruby.handler: |
          Proc.new do |env|
            [200, {}, ["#{env["SCRIPT_NAME"]}#{env["PATH_INFO"]};#{env["CONTENT_LENGTH"]}"]]
          end
EOT
    my ($stderr, $stdout) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    is $stdout, "/dest/;";
    ($stderr, $stdout) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/hoge");
    is $stdout, "/dest/hoge;";
    subtest "preserve-method" => sub {
        ($stderr, $stdout) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/307/");
        is $stdout, "/dest/;";
        ($stderr, $stdout) = run_prog("curl --data hello --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/307/");
        is $stdout, "/dest/;5";
        ($stderr, $stdout) = run_prog("curl --data hello --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
        is $stdout, "/dest/;";
    };
};

subtest "server-push" => sub {
    plan skip_all => 'nghttp not found'
        unless prog_exists('nghttp');
    my $server = spawn_h2o(<< "EOT");
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          Proc.new do |env|
            push_paths = []
            if env["PATH_INFO"] == "/index.txt"
              push_paths << "/index.js"
            end
            [399, push_paths.empty? ? {} : {"link" => push_paths.map{|p| "<#{p}>; rel=preload"}.join()}, []]
          end
        file.dir: t/assets/doc_root
EOT
    my $resp = `nghttp -n --stat https://127.0.0.1:$server->{tls_port}/index.txt`;
    like $resp, qr{\nid\s*responseEnd\s.*\s/index\.js\n.*\s/index\.txt}is, "receives index.js then /index.txt";
};

subtest "server-push / nopush" => sub {
    plan skip_all => 'nghttp not found'
        unless prog_exists('nghttp');
    my $server = spawn_h2o(<< "EOT");
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          Proc.new do |env|
            push_paths = []
            if env["PATH_INFO"] == "/index.txt"
              push_paths << "/index.js"
            end
            [399, push_paths.empty? ? {} : {"link" => push_paths.map{|p| "<#{p}>; rel=preload; nopush"}.join()}, []]
          end
        file.dir: t/assets/doc_root
EOT
    my $resp = `nghttp -n --stat https://127.0.0.1:$server->{tls_port}/index.txt`;
    unlike $resp, qr{/index\.js}is, "receives only /index.txt";
    like $resp, qr{/index\.txt}is, "receives only /index.txt";
};

subtest "infinite-reprocess" => sub {
    my $server = spawn_h2o(sub {
        my ($port, $tls_port) = @_;
        return << "EOT";
hosts:
  "127.0.0.1:$port":
    paths:
      /:
        reproxy: ON
        mruby.handler: |
          Proc.new do |env|
            [200,{"x-reproxy-url" => "http://127.0.0.1:$port/"},[]]
          end
EOT
    });
    my ($stderr, $stdout) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    like $stderr, qr{^HTTP\/1.1 502 }s, "502 response";
    like $stdout, qr{too many internal delegations}, "reason";
};

subtest "send-file" => sub {
    my $server = spawn_h2o(<< "EOT");
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          Proc.new do |env|
            [200,{}, File::open("t/50mruby/index.html")]
          end
EOT
    my ($headers, $body) = run_prog("curl --silent -A h2o_mruby_test --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    like $headers, qr{^HTTP/1\.1 200 OK\r\n}is;
    is md5_hex($body), md5_file("t/50mruby/index.html");
};

subtest "exception" => sub {
    my $server = spawn_h2o(<< 'EOT');
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          cnt = 0
          Proc.new do |env|
            cnt += 1
            if cnt % 2 != 0
              [200, {}, ["hello\n"]]
            else
              raise "error from rack"
            end
          end
EOT
    my $fetch = sub {
        run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}");
    };
    for (1..3) {
        my ($headers, $body) = $fetch->();
        like $headers, qr{^HTTP/1\.1 200 }is;
        is $body, "hello\n";
        ($headers, $body) = $fetch->();
        like $headers, qr{^HTTP/1\.1 500 }is;
    }
};

subtest "post" => sub {
    my $server = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          Proc.new do |env|
            body = []
            3.times do
              env["rack.input"].rewind
              body << env["rack.input"].read
              body << "\\n"
            end
            [200, {}, body]
          end
EOT
    my ($headers, $body) = run_prog("curl --silent --data 'hello' --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    like $headers, qr{^HTTP/1\.1 200 OK\r\n}is;
    is $body, "hello\n" x 3;
};

subtest "InputStream#read-after-close" => sub {
    my $server = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          prev_input = nil
          Proc.new do |env|
            if !prev_input
              prev_input = env["rack.input"]
              resp = "not cached"
            else
              begin
                prev_input.read
                resp = "must not seed this"
              rescue IOError => e
                resp = "got IOError"
              end
            end
            [200, {}, [resp]]
          end
EOT
    my ($headers, $body) = run_prog("curl --silent --data 'hello' --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    like $headers, qr{^HTTP/1\.1 200 OK\r\n}is;
    is $body, "not cached";
    ($headers, $body) = run_prog("curl --silent --data 'hello' --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    like $headers, qr{^HTTP/1\.1 200 OK\r\n}is;
    is $body, "got IOError";
};

subtest "header-concat" => sub {
    my $server = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          Proc.new do |env|
            [200, {}, [env["HTTP_COOKIE"]]]
          end
EOT
    run_with_curl($server, sub {
        my ($proto, $port, $curl) = @_;
        my ($headers, $body) = run_prog("$curl --silent -H 'cookie: a=b' -H 'cookie: c=d' --dump-header /dev/stderr $proto://127.0.0.1:$port/");
        like $headers, qr{^HTTP/\S+ 200}is;
        like $body, qr{^a=b;\s*c=d$}is;
    });
};

subtest "close-called" => sub {
    my $server = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          is_open = false
          lambda do |env|
            if is_open
              return [500, {}, ["close not called"]]
            end
            is_open = true
            return [
              200,
              {},
              Class.new do
                def each
                  yield "hello"
                end
                define_method(:close) do
                  is_open = false
                end
              end.new,
            ]
          end
EOT
    my ($headers, $body) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    like $headers, qr{^HTTP/1\.1 200 }is;
    is $body, "hello";
    ($headers, $body) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    like $headers, qr{^HTTP/1\.1 200 }is;
    is $body, "hello";
};

subtest "close-called-on-exception" => sub {
    my $server = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          is_open = false
          lambda do |env|
            if is_open
              return [500, {}, ["close not called"]]
            end
            is_open = true
            return [
              200,
              {},
              Class.new do
                def each
                  yield "hello"
                  raise "yeah!"
                end
                define_method(:close) do
                  is_open = false
                end
              end.new,
            ]
          end
EOT
    my ($headers, $body) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    like $headers, qr{^HTTP/1\.1 200 }is;
    is $body, "hello";
    ($headers, $body) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    like $headers, qr{^HTTP/1\.1 200 }is;
    is $body, "hello";
};

subtest "log lineno" => sub {
    my $tester = sub {
        my ($name, $conf, $lineno) = @_;

        subtest $name => sub {
            my $tempdir = tempdir(CLEANUP => 1);
            unlink "$tempdir/error_log";
            my $server = spawn_h2o(<< "EOT");
$conf
error-log: $tempdir/error_log
EOT
            run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
            undef $server; # wait for the server to terminate
            my @log = do {
                open my $fh, "<", "$tempdir/error_log"
                    or die "failed to open error_log:$!";
                map { my $l = $_; chomp $l; $l } <$fh>;
            };
            @log = grep { $_ =~ /^\[h2o_mruby\]/ } @log;
            like $log[$#log], qr{\[h2o_mruby\] in request:127\.0\.0\.1:\d+/:mruby raised:\s+(:$lineno:|)\s*hoge \(RuntimeError\)};
        };
    };
    $tester->("flow style", <<"EOT", 6);
hosts:
  default:
    paths:
      /:
        mruby.handler: Proc.new do |env| raise "hoge" end
EOT
    $tester->("block style", <<"EOT", 8);
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          Proc.new do |env|
            raise "hoge"
          end
EOT
};

subtest 'namespace' => sub {
    subtest 'modules and classes are defined under top_self (not under Kernel)' => sub {
        my $server = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          module Foo
          end
          class Bar
          end
          proc {|env|
            [200, {}, ["#{Foo.name},#{Bar.name}"]]
          }
EOT
        (undef, my $body) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
        is $body, "Foo,Bar";
    };
    subtest 'require works as the same' => sub {
        my $server = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          \$LOAD_PATH << '@{[ ASSETS_DIR . '/mruby' ]}'
          require 'namespace'
          proc {|env|
            [200, {}, ["#{Foo.name},#{Bar.name}"]]
          }
EOT
        (undef, my $body) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
        like $body, qr{(Kernel::|)Foo,(Kernel::|)Bar};
    };
    subtest 'self must be top_self' => sub {
        my $server = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          proc {|env|
            [200, {}, [self.to_s]]
          }
EOT
        (undef, my $body) = run_prog("curl --silent --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
        is $body, "main";
    };
};

subtest 'response with specific statuses should not contain content-length header' => sub {
    my $server = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          proc {|env|
            [204, {}, []]
          }
EOT
    my ($headers, $body) = run_prog("curl --silent --data 'hello' --dump-header /dev/stderr http://127.0.0.1:$server->{port}/");
    like $headers, qr{^HTTP/1\.1 204 OK\r\n}is;
    unlike $headers, qr{^content-length:}im;
};

subtest 'PATH_INFO and SCRIPT_NAME' => sub {
    plan skip_all => "nc not found"
        unless prog_exists("nc");

    my $server = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          proc {|env|
            [200, {}, ['handler1, ' + env['SCRIPT_NAME'] + ', ' + env['PATH_INFO']]]
          }
      /abc:
        mruby.handler: |
          proc {|env|
            [200, {}, ['handler2, ' + env['SCRIPT_NAME'] + ', ' + env['PATH_INFO']]]
          }
      "/foo bar":
        mruby.handler: |
          proc {|env|
            [200, {}, ['handler3, ' + env['SCRIPT_NAME'] + ', ' + env['PATH_INFO']]]
          }
EOT
    my $nc = sub {
        my $path = shift;
        my $cmd = "echo 'GET $path HTTP/1.1\r\nHost: 127.0.0.1\r\n\r' | nc 127.0.0.1 $server->{port}";
        (undef, my $r) = run_prog($cmd);
        split(/\r\n\r\n/, $r, 2);
    };

    my $body;
    (undef, $body) = $nc->('/abc/def%20ghi');
    is $body, 'handler2, /abc, /def%20ghi', 'should be kept undecoded';

    (undef, $body) = $nc->('/abc/def/../ghi/../jhk');
    is $body, 'handler2, /abc, /def/../ghi/../jhk', 'https://github.com/h2o/h2o/pull/1480#issuecomment-339614160';

    (undef, $body) = $nc->('/123/../abc/def/../ghi');
    is $body, 'handler2, /abc, /def/../ghi', 'https://github.com/h2o/h2o/pull/1480#issuecomment-339658134';

    (undef, $body) = $nc->('/foo%20bar/baz');
    is $body, 'handler3, /foo bar, /baz', 'paths should be decoded';

    (undef, $body) = $nc->('/xxx/../hoge');
    is $body, 'handler1, , /xxx/../hoge', 'string size is too big issue 1';

    (undef, $body) = $nc->('/../abc');
    is $body, 'handler2, /abc, ', 'string size is too big issue 2';

    (undef, $body) = $nc->('abc');
    is $body, 'handler2, /abc, ', 'no leading slash 1';

    (undef, $body) = $nc->('abc/def');
    is $body, 'handler2, /abc, /def', 'no leading slash 2';

    (undef, $body) = $nc->('123/../abc/def/../ghi');
    is $body, 'handler2, /abc, /def/../ghi', 'no leading slash 3';

    (undef, $body) = $nc->('xyz');
    is $body, 'handler1, , xyz', 'no leading slash 4';
};

subtest 'invalid response' => sub {
    my $server = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          proc {|env| eval([env["QUERY_STRING"]].pack("H*")) }
EOT
    run_with_curl($server, sub {
        my ($proto, $port, $curl) = @_;
        my $fetch = sub {
            run_prog("$curl --silent --dump-header /dev/stderr -m 1 $proto://127.0.0.1:$port/?@{[unpack 'H*', $_[0]]}");
        };
        my ($headers, $body) = $fetch->('[200, {}, ["hello world"]]');
        subtest "verify the methodology" => sub {
            like $headers, qr{^HTTP/[0-9.]+ 200 }is;
            is $body, "hello world";
        };
        ($headers, $body) = $fetch->('nil');
        like $headers, qr{^HTTP/[0-9.]+ 500 }is, 'nil';
        ($headers, $body) = $fetch->('["200", {}, []]');
        like $headers, qr{^HTTP/[0-9.]+ 500 }is, 'invalid status';
        ($headers, $body) = $fetch->('[200]');
        like $headers, qr{^HTTP/[0-9.]+ 500 }is, 'no headers';
        ($headers, $body) = $fetch->('[200, nil, nil]');
        like $headers, qr{^HTTP/[0-9.]+ 500 }is, 'nil headers';
        ($headers, $body) = $fetch->('[200, "abc", nil]');
        like $headers, qr{^HTTP/[0-9.]+ 500 }is, 'invalid headers';
        ($headers, $body) = $fetch->('[200, {}]');
        like $headers, qr{^HTTP/[0-9.]+ 200 }is, 'no body';
        is $body, "";
        ($headers, $body) = $fetch->('[200, {}, nil]');
        like $headers, qr{^HTTP/[0-9.]+ 200 }is, 'nil body';
        is $body, "";
        ($headers, $body) = $fetch->('[200, {}, "abc"]');
        like $headers, qr{^HTTP/[0-9.]+ 200 }is, 'invalid body';
        is $body, "";
    });
};

subtest 'rack.early_hints' => sub {
    my $server = spawn_h2o(<< "EOT");
send-informational: all
num-threads: 1
hosts:
  default:
    paths:
      /:
        mruby.handler: |
          proc {|env|
            env['rack.early_hints'].call({ 'link' => "</index.js>; rel=preload\\n</style.css>; rel=preload" })
            sleep 0.1
            env['rack.early_hints'].call({ :foo => 'bar' })
            [200, {}, ['hello']]
          }
EOT
    run_with_curl($server, sub {
        my ($proto, $port, $curl) = @_;
        my ($headers) = run_prog("$curl --silent --dump-header /dev/stderr -m 1 $proto://127.0.0.1:$port/");
        like $headers, qr{^link: </index.js>; rel=preload\r$}m;
        like $headers, qr{^link: </style.css>; rel=preload\r$}m;
        like $headers, qr{^foo: bar\r$}m;
    });
};

done_testing();
