// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:test/test.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

Map<String, String> get _handshakeHeaders => {
      'Upgrade': 'websocket',
      'Connection': 'Upgrade',
      'Sec-WebSocket-Key': 'x3JJHMbDL1EzLkh9GBhXDw==',
      'Sec-WebSocket-Version': '13'
    };

void main() {
  test('can communicate with a dart:io WebSocket client', () async {
    final server =
        await shelf_io.serve(webSocketHandler((WebSocketChannel webSocket) {
      webSocket.sink.add('hello!');
      webSocket.stream.first.then((request) {
        expect(request, equals('ping'));
        webSocket.sink.add('pong');
        webSocket.sink.close();
      });
    }), 'localhost', 0);

    try {
      final webSocket =
          await WebSocket.connect('ws://localhost:${server.port}');
      var n = 0;
      await webSocket.listen((message) {
        if (n == 0) {
          expect(message, equals('hello!'));
          webSocket.add('ping');
        } else if (n == 1) {
          expect(message, equals('pong'));
          webSocket.close();
          server.close();
        } else {
          fail('Only expected two messages.');
        }
        n++;
      }).asFuture<void>();
    } finally {
      await server.close();
    }
  });

  test('negotiates the sub-protocol', () async {
    final server = await shelf_io.serve(
        webSocketHandler((WebSocketChannel webSocket, String? protocol) {
          expect(protocol, equals('two'));
          webSocket.sink.close();
        }, protocols: ['three', 'two', 'x']),
        'localhost',
        0);

    try {
      final webSocket = await WebSocket.connect('ws://localhost:${server.port}',
          protocols: ['one', 'two', 'three']);
      expect(webSocket.protocol, equals('two'));
      return webSocket.close();
    } finally {
      await server.close();
    }
  });

  test('handles protocol header without allowed protocols', () async {
    final server =
        await shelf_io.serve(webSocketHandler((WebSocketChannel webSocket) {
      webSocket.sink.close();
    }), 'localhost', 0);

    try {
      final webSocket = await WebSocket.connect('ws://localhost:${server.port}',
          protocols: ['one', 'two', 'three']);
      expect(webSocket.protocol, isNull);
      return webSocket.close();
    } finally {
      await server.close();
    }
  });

  test('allows two argument callbacks without protocols', () async {
    final server = await shelf_io.serve(
        webSocketHandler((WebSocketChannel webSocket, String? protocol) {
      expect(protocol, isNull);
      webSocket.sink.close();
    }), 'localhost', 0);

    try {
      final webSocket = await WebSocket.connect('ws://localhost:${server.port}',
          protocols: ['one', 'two', 'three']);
      expect(webSocket.protocol, isNull);
      return webSocket.close();
    } finally {
      await server.close();
    }
  });

  test('cannot hijack non-Socket StreamChannel', () async {
    final handler =
        webSocketHandler((WebSocketChannel webSocket, String? protocol) {
      expect(protocol, isNull);
      webSocket.sink.close();
    });

    expect(
        () => handler(Request('GET', Uri.parse('ws://example.com/'),
                protocolVersion: '1.1',
                headers: {
                  'Host': 'example.com',
                  'Upgrade': 'websocket',
                  'Connection': 'Upgrade',
                  'Sec-WebSocket-Key': 'x3JJHMbDL1EzLkh9GBhXDw==',
                  'Sec-WebSocket-Version': '13',
                  'Origin': 'http://example.com',
                }, onHijack: (fn) {
              // `.foreign` is not a Socket so hijacking the request should
              // fail.
              expect(() => fn(StreamChannelController<List<int>>().foreign),
                  throwsArgumentError);
            })),
        throwsA(isA<HijackException>()));
  });

  group('with a set of allowed origins', () {
    late HttpServer server;
    late Uri url;
    setUp(() async {
      server = await shelf_io.serve(
          webSocketHandler((WebSocketChannel webSocket) {
            webSocket.sink.close();
          }, allowedOrigins: ['pub.dartlang.org', 'GoOgLe.CoM']),
          'localhost',
          0);
      url = Uri.http('localhost:${server.port}', '');
    });

    tearDown(() => server.close());

    test('allows access with an allowed origin', () {
      final headers = _handshakeHeaders;
      headers['Origin'] = 'pub.dartlang.org';
      expect(http.get(url, headers: headers), hasStatus(101));
    });

    test('forbids access with a non-allowed origin', () {
      final headers = _handshakeHeaders;
      headers['Origin'] = 'dartlang.org';
      expect(http.get(url, headers: headers), hasStatus(403));
    });

    test('allows access with no origin', () {
      expect(http.get(url, headers: _handshakeHeaders), hasStatus(101));
    });

    test('ignores the case of the client origin', () {
      final headers = _handshakeHeaders;
      headers['Origin'] = 'PuB.DaRtLaNg.OrG';
      expect(http.get(url, headers: headers), hasStatus(101));
    });

    test('ignores the case of the server origin', () {
      final headers = _handshakeHeaders;
      headers['Origin'] = 'google.com';
      expect(http.get(url, headers: headers), hasStatus(101));
    });
  });

  // Regression test for issue 21894.
  test('allows a Connection header with multiple values', () async {
    final server =
        await shelf_io.serve(webSocketHandler((WebSocketChannel webSocket) {
      webSocket.sink.close();
    }), 'localhost', 0);

    final url = Uri.http('localhost:${server.port}', '');
    final headers = _handshakeHeaders;
    headers['Connection'] = 'Other-Token, Upgrade';
    expect(http.get(url, headers: headers).whenComplete(server.close),
        hasStatus(101));
  });

  group('HTTP errors', () {
    late HttpServer server;
    late Uri url;
    setUp(() async {
      server = await shelf_io.serve(webSocketHandler((_) {
        fail('should not create a WebSocket');
      }), 'localhost', 0);
      url = Uri.http('localhost:${server.port}', '');
    });

    tearDown(() => server.close());

    test('404s for non-GET requests', () {
      expect(http.delete(url, headers: _handshakeHeaders), hasStatus(404));
    });

    test('404s for non-Upgrade requests', () {
      final headers = _handshakeHeaders..remove('Connection');
      expect(http.get(url, headers: headers), hasStatus(404));
    });

    test('404s for non-websocket upgrade requests', () {
      final headers = _handshakeHeaders;
      headers['Upgrade'] = 'fblthp';
      expect(http.get(url, headers: headers), hasStatus(404));
    });

    test('400s for a missing Sec-WebSocket-Version', () {
      final headers = _handshakeHeaders..remove('Sec-WebSocket-Version');
      expect(http.get(url, headers: headers), hasStatus(400));
    });

    test('404s for an unknown Sec-WebSocket-Version', () {
      final headers = _handshakeHeaders;
      headers['Sec-WebSocket-Version'] = '15';
      expect(http.get(url, headers: headers), hasStatus(404));
    });

    test('400s for a missing Sec-WebSocket-Key', () {
      final headers = _handshakeHeaders..remove('Sec-WebSocket-Key');
      expect(http.get(url, headers: headers), hasStatus(400));
    });
  });
}

Matcher hasStatus(int status) => completion(predicate((http.Response response) {
      expect(response, isA<http.Response>());
      expect(response.statusCode, equals(status));
      return true;
    }));
