<?php

class WP_Test_WC_Connect_Shipping_Label extends WC_Unit_Test_Case {

	protected $wc_connect_packages = array(
		array(
			'id'         => 'weight_0_.',
			'box_id'     => '"',
			'length'     => 10,
			'width'      => 10,
			'height'     => 10,
			'weight'     => 0.25625,
			'items'      => array(
				array(
					'product_id' => 128,
					'length'     => 3,
					'width'      => 3,
					'height'     => 2.5,
					'weight'     => 0.15625,
					'quantity'   => 1,
				),
			),
			'service_id' => 'pri',
		),
	);

	protected $wc_connect_packages_multiple = array(
		array(
			'id'         => 'weight_0_.',
			'box_id'     => '"',
			'length'     => 10,
			'width'      => 10,
			'height'     => 10,
			'weight'     => 0.25625,
			'items'      => array(
				array(
					'product_id' => 128,
					'length'     => 3,
					'width'      => 3,
					'height'     => 2.5,
					'weight'     => 0.15625,
					'quantity'   => 1,
				),
			),
			'service_id' => 'pri',
		),
		array(
			'id'         => 'weight_1_.',
			'box_id'     => '"',
			'length'     => 10,
			'width'      => 10,
			'height'     => 10,
			'weight'     => 0.25625,
			'items'      => array(
				array(
					'product_id' => 128,
					'length'     => 3,
					'width'      => 3,
					'height'     => 2.5,
					'weight'     => 0.15625,
					'quantity'   => 1,
				),
				array(
					'product_id' => 129,
					'length'     => 3,
					'width'      => 3,
					'height'     => 2.5,
					'weight'     => 0.15625,
					'quantity'   => 1,
				),
			),
			'service_id' => 'pri',
		),
	);

	protected $expected_selected_packages = array(
		'weight_0_.' => array(
			'id'         => 'weight_0_.',
			'box_id'     => '"',
			'length'     => 10,
			'width'      => 10,
			'height'     => 10,
			'weight'     => 0.25625,
			'items'      => array(
				array(
					'product_id' => 128,
					'value'      => 0,
					'length'     => 3,
					'width'      => 3,
					'height'     => 2.5,
					'weight'     => 0.15625,
					'quantity'   => 1,
					'name'       => '#128 - [Deleted product]',
				),
			),
			'service_id' => 'pri',
		),
	);

	protected $expected_selected_packages_multiple = array(
		'weight_0_.' => array(
			'id'         => 'weight_0_.',
			'box_id'     => '"',
			'length'     => 10,
			'width'      => 10,
			'height'     => 10,
			'weight'     => 0.25625,
			'items'      => array(
				array(
					'product_id' => 128,
					'value'      => 0,
					'length'     => 3,
					'width'      => 3,
					'height'     => 2.5,
					'weight'     => 0.15625,
					'quantity'   => 1,
					'name'       => '#128 - [Deleted product]',
				),
			),
			'service_id' => 'pri',
		),
		'weight_1_.' => array(
			'id'         => 'weight_1_.',
			'box_id'     => '"',
			'length'     => 10,
			'width'      => 10,
			'height'     => 10,
			'weight'     => 0.25625,
			'items'      => array(
				array(
					'product_id' => 128,
					'value'      => 0,
					'length'     => 3,
					'width'      => 3,
					'height'     => 2.5,
					'weight'     => 0.15625,
					'quantity'   => 1,
					'name'       => '#128 - [Deleted product]',
				),
				array(
					'product_id' => 129,
					'value'      => 0,
					'length'     => 3,
					'width'      => 3,
					'height'     => 2.5,
					'weight'     => 0.15625,
					'quantity'   => 1,
					'name'       => '#129 - [Deleted product]',
				),
			),
			'service_id' => 'pri',
		),
	);

	public static function set_up_before_class() {
		require_once dirname( __FILE__ ) . '/../../classes/class-wc-connect-compatibility.php';
		require_once dirname( __FILE__ ) . '/../../classes/class-wc-connect-shipping-label.php';
		require_once dirname( __FILE__ ) . '/../../classes/class-wc-connect-service-settings-store.php';
		require_once dirname( __FILE__ ) . '/../../classes/class-wc-connect-api-client.php';
		require_once dirname( __FILE__ ) . '/../../classes/class-wc-connect-api-client-live.php';
		require_once dirname( __FILE__ ) . '/../../classes/class-wc-connect-service-schemas-store.php';
		require_once dirname( __FILE__ ) . '/../../classes/class-wc-connect-payment-methods-store.php';
		require_once dirname( __FILE__ ) . '/../../classes/class-wc-connect-account-settings.php';
		require_once dirname( __FILE__ ) . '/../../classes/class-wc-connect-package-settings.php';
		require_once dirname( __FILE__ ) . '/../../classes/class-wc-connect-continents.php';

		WC_Connect_Compatibility::set_version( '3.0.0' );
	}

	private function get_shipping_label( $api_client = false, $settings_store = false, $service_schemas_store = false, $payment_methods_store = false ) {
		if ( ! $settings_store ) {
			$settings_store = $this->getMockBuilder( 'WC_Connect_Service_Settings_Store' )
				 ->disableOriginalConstructor()
				 ->setMethods( null )
				 ->getMock();
		}

		if ( ! $api_client ) {
			$api_client = $this->getMockBuilder( 'WC_Connect_API_Client_Live' )
				->disableOriginalConstructor()
				->setMethods( null )
				->getMock();
		}

		if ( ! $service_schemas_store ) {
			$service_schemas_store = $this->getMockBuilder( 'WC_Connect_Service_Schemas_Store' )
				->disableOriginalConstructor()
				->setMethods( null )
				->getMock();
		}

		if ( ! $payment_methods_store ) {
			$payment_methods_store = $this->getMockBuilder( 'WC_Connect_Payment_Methods_Store' )
				->disableOriginalConstructor()
				->setMethods( null )
				->getMock();
		}

		return new WC_Connect_Shipping_Label( $api_client, $settings_store, $service_schemas_store, $payment_methods_store );
	}

	private function create_mock_order() {
		$mock_order = $this->getMockBuilder( 'WC_Order' )
			->disableOriginalConstructor()
			->setMethods( array( 'get_shipping_methods', 'get_items' ) )
			->getMock();

		$mock_order->expects( $this->any() )->method( 'get_items' )->will( $this->returnValue( array() ) );

		return $mock_order;
	}

	public function test_get_selected_rates_regular_json() {
		$json = json_encode( $this->wc_connect_packages );

		$shipping_method = array(
			array(
				'wc_connect_packages' => $json,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$expected = array(
			'weight_0_.' => 'pri',
		);

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_rates( $mock_order );
		$this->assertEquals( $actual, $expected );
	}

	public function test_get_selected_rates_unescaped_json() {
		// create a json and ensure that quotes are unescaped
		$json = json_encode( $this->wc_connect_packages );
		$json = str_replace( '"box_id":"\""', '"box_id":"""', $json );

		$shipping_method = array(
			array(
				'wc_connect_packages' => $json,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$expected = array(
			'weight_0_.' => 'pri',
		);

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_rates( $mock_order );
		$this->assertEquals( $actual, $expected );
	}

	public function test_get_selected_rates_serialized_array() {
		$shipping_method = array(
			array(
				'wc_connect_packages' => maybe_serialize( $this->wc_connect_packages ),
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$expected = array(
			'weight_0_.' => 'pri',
		);

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_rates( $mock_order );
		$this->assertEquals( $actual, $expected );
	}

	public function test_get_selected_rates_unserialized_array() {
		$shipping_method = array(
			array(
				'wc_connect_packages' => $this->wc_connect_packages,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$expected = array(
			'weight_0_.' => 'pri',
		);

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_rates( $mock_order );
		$this->assertEquals( $actual, $expected );
	}

	public function test_get_selected_packages_regular_json() {
		$json = json_encode( $this->wc_connect_packages );

		$shipping_method = array(
			array(
				'wc_connect_packages' => $json,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_packages( $mock_order );
		$this->assertEquals( $actual, $this->expected_selected_packages );
	}

	public function test_get_selected_packages_unescaped_json() {
		// create a json and ensure that quotes are unescaped
		$json = json_encode( $this->wc_connect_packages );
		$json = str_replace( '"box_id":"\""', '"box_id":"""', $json );

		$shipping_method = array(
			array(
				'wc_connect_packages' => $json,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_packages( $mock_order );
		$this->assertEquals( $actual, $this->expected_selected_packages );
	}

	public function test_get_selected_packages_serialized_array() {
		$shipping_method = array(
			array(
				'wc_connect_packages' => maybe_serialize( $this->wc_connect_packages ),
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_packages( $mock_order );
		$this->assertEquals( $actual, $this->expected_selected_packages );
	}

	public function test_get_selected_packages_unserialized_array() {
		$shipping_method = array(
			array(
				'wc_connect_packages' => $this->wc_connect_packages,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_packages( $mock_order );
		$this->assertEquals( $actual, $this->expected_selected_packages );
	}

	// WC 3.1.0 shipping rate meta_data save bug
	// See: https://github.com/Automattic/woocommerce-services/issues/1075
	public function test_get_selected_packages_null_in_array() {
		$shipping_method = array(
			array(
				'wc_connect_packages' => array( null ), // Bug causes data to be `a:1:{i:0;N;}` in database
			),
		);

		$expected = array(
			'default_box' => array(
				'id'     => 'default_box',
				'box_id' => 'not_selected',
				'height' => 0,
				'length' => 0,
				'weight' => 0,
				'width'  => 0,
				'items'  => array(),
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_packages( $mock_order );
		$this->assertEquals( $actual, $expected );
	}

	public function test_get_selected_rates_multiple_packages_regular_json() {
		$json = json_encode( $this->wc_connect_packages_multiple );

		$shipping_method = array(
			array(
				'wc_connect_packages' => $json,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$expected = array(
			'weight_0_.' => 'pri',
			'weight_1_.' => 'pri',
		);

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_rates( $mock_order );
		$this->assertEquals( $actual, $expected );
	}

	public function test_get_selected_rates_multiple_packages_unescaped_json() {
		// create a json and ensure that quotes are unescaped
		$json = json_encode( $this->wc_connect_packages_multiple );
		$json = str_replace( '"box_id":"\""', '"box_id":"""', $json );

		$shipping_method = array(
			array(
				'wc_connect_packages' => $json,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$expected = array(
			'weight_0_.' => 'pri',
			'weight_1_.' => 'pri',
		);

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_rates( $mock_order );
		$this->assertEquals( $actual, $expected );
	}

	public function test_get_selected_rates_multiple_packages_serialized_array() {
		$shipping_method = array(
			array(
				'wc_connect_packages' => maybe_serialize( $this->wc_connect_packages_multiple ),
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$expected = array(
			'weight_0_.' => 'pri',
			'weight_1_.' => 'pri',
		);

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_rates( $mock_order );
		$this->assertEquals( $actual, $expected );
	}

	public function test_get_selected_rates_multiple_packages_unserialized_array() {
		$shipping_method = array(
			array(
				'wc_connect_packages' => $this->wc_connect_packages_multiple,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$expected = array(
			'weight_0_.' => 'pri',
			'weight_1_.' => 'pri',
		);

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_rates( $mock_order );
		$this->assertEquals( $actual, $expected );
	}

	public function test_get_selected_packages_multiple_packages_regular_json() {
		$json = json_encode( $this->wc_connect_packages_multiple );

		$shipping_method = array(
			array(
				'wc_connect_packages' => $json,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_packages( $mock_order );
		$this->assertEquals( $actual, $this->expected_selected_packages_multiple );
	}

	public function test_get_selected_packages_multiple_packages_unescaped_json() {
		// create a json and ensure that quotes are unescaped
		$json = json_encode( $this->wc_connect_packages_multiple );
		$json = str_replace( '"box_id":"\""', '"box_id":"""', $json );

		$shipping_method = array(
			array(
				'wc_connect_packages' => $json,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_packages( $mock_order );
		$this->assertEquals( $actual, $this->expected_selected_packages_multiple );
	}

	public function test_get_selected_packages_multiple_packages_serialized_array() {
		$shipping_method = array(
			array(
				'wc_connect_packages' => maybe_serialize( $this->wc_connect_packages_multiple ),
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_packages( $mock_order );
		$this->assertEquals( $actual, $this->expected_selected_packages_multiple );
	}

	public function test_get_selected_packages_multiple_packages_unserialized_array() {
		$shipping_method = array(
			array(
				'wc_connect_packages' => $this->wc_connect_packages_multiple,
			),
		);

		$mock_order = $this->create_mock_order();
		$mock_order->expects( $this->any() )->method( 'get_shipping_methods' )->will( $this->returnValue( $shipping_method ) );

		$shipping_label = $this->get_shipping_label();
		$actual         = $shipping_label->get_selected_packages( $mock_order );
		$this->assertEquals( $actual, $this->expected_selected_packages_multiple );
	}

	/**
	 * Test that an order with one virtual product is not eligible for shipping label creation (shipping is not required).
	 */
	public function test_order_with_a_virtual_product_is_not_eligible_for_shipping_label_creation() {
		// Given.
		$product = $this->create_simple_product( true );
		$order   = WC_Helper_Order::create_order( 1, $product );

		// When.
		$shipping_label = $this->get_shipping_label();
		$is_eligible    = $shipping_label->is_order_eligible_for_shipping_label_creation( $order );

		// Then.
		$this->assertFalse( $is_eligible );
	}

	/**
	 * Test that an order with one shippable product is eligible for shipping label creation.
	 */
	public function test_order_with_a_shippable_product_is_eligible_for_shipping_label_creation() {
		// Given.
		$product = $this->create_simple_product( false );
		$order   = WC_Helper_Order::create_order( 1, $product );

		// When.
		$shipping_label = $this->get_shipping_label();
		$is_eligible    = $shipping_label->is_order_eligible_for_shipping_label_creation( $order );

		// Then.
		$this->assertTrue( $is_eligible );
	}

	/**
	 * Test that an order with one fully refunded shippable product is not eligible for shipping label creation (no product to ship).
	 */
	public function test_order_with_a_fully_refunded_shippable_product_is_not_eligible_for_shipping_label_creation() {
		// Given.
		$product = $this->create_simple_product( false );
		$order   = WC_Helper_Order::create_order( 1, $product );

		$order_items       = $order->get_items();
		$refund_line_items = array();
		foreach ( $order_items as $order_item ) {
			$refund_line_items[ $order_item->get_id() ] = array(
				'qty'          => $order_item->get_quantity(),
				'refund_total' => 20,
			);
		}
		$refund_args = array(
			'order_id'   => $order->get_id(),
			'line_items' => $refund_line_items,
		);
		wc_create_refund( $refund_args );

		// When.
		$shipping_label = $this->get_shipping_label();
		$is_eligible    = $shipping_label->is_order_eligible_for_shipping_label_creation( $order );

		// Then.
		$this->assertFalse( $is_eligible );
	}

	/**
	 * Test that an order with one partially refunded shippable product is eligible for shipping label creation (still product to ship).
	 */
	public function test_order_with_a_partially_refunded_shippable_product_is_eligible_for_shipping_label_creation() {
		// Given.
		$product = $this->create_simple_product( false );
		$order   = WC_Helper_Order::create_order( 1, $product );

		$order_items       = $order->get_items();
		$refund_line_items = array();
		foreach ( $order_items as $order_item ) {
			$refund_line_items[ $order_item->get_id() ] = array(
				'qty'          => $order_item->get_quantity() - 1,
				'refund_total' => 20,
			);
		}
		$refund_args = array(
			'order_id'   => $order->get_id(),
			'line_items' => $refund_line_items,
		);
		wc_create_refund( $refund_args );

		// When.
		$shipping_label = $this->get_shipping_label();
		$is_eligible    = $shipping_label->is_order_eligible_for_shipping_label_creation( $order );

		// Then.
		$this->assertTrue( $is_eligible );
	}

	/**
	 * Test that get_all_items() returns only order items that have not been refunded.
	 */
	public function test_get_all_items_method_only_retrieves_non_refunded_order_items() {
		// Given.
		$product = $this->create_simple_product( false );

		$order = WC_Helper_Order::create_order(); // Note: this helper method already adds one product with a qty of 4.
		$order->set_status( 'on-hold' );
		$order->add_product( $product, 6 );
		$order->save();

		$order_items       = $order->get_items();
		$refund_line_items = array();
		foreach ( $order_items as $order_item ) {
			$refund_line_items[ $order_item->get_id() ] = array(
				'qty'          => $order_item->get_quantity() - 3,
				'refund_total' => 20,
			);
		}
		$refund_args = array(
			'order_id'   => $order->get_id(),
			'line_items' => $refund_line_items,
		);
		wc_create_refund( $refund_args );

		// When.
		$shipping_label  = $this->get_shipping_label();
		$all_items       = $shipping_label->get_all_items( $order );
		$all_items_count = count( $all_items );

		// Then.
		$this->assertEquals( 6, $all_items_count );
	}

	public function test_shipment_for_items_with_big_quantity_is_capped() {

		add_filter( 'wc_connect_shipment_item_quantity_threshold', function (){
			return 20;
		} );
		add_filter( 'wc_connect_max_shipments_if_quantity_exceeds_threshold', function (){
			return 5;
		} );

		$product_A = $this->create_simple_product();
		$product_A->save();

		$product_B = $this->create_simple_product();
		$product_B->save();

		/**
		 * @var WC_Order $order
		 */
		$order = WC_Helper_Order::create_order(); // Note: this helper method already adds one product with a qty of 4.
		$order->remove_order_items();
		$order->set_status( 'on-hold' );
		$order->add_product( $product_A, 32 );
		$order->add_product( $product_B, 19 );
		$order->save();

		$shipping_label  = $this->get_shipping_label();
		$all_items       = $shipping_label->get_all_items( $order );

		$all_items_of_product_A_type = array_values(
				array_filter( $all_items, function ($item) use($product_A) {
					return $item['product_id'] === $product_A->get_id();
				})
		);


		/**
		 * An item with quantity = 32 would yield 5 shipments all having 6 quantity and  the last shipment having 8.
		 */
		$this->assertEquals( 5, count( $all_items_of_product_A_type ) );
		$this->assertEquals( 6, $all_items_of_product_A_type[0]['quantity'] );
		$this->assertEquals( 6, $all_items_of_product_A_type[1]['quantity'] );
		$this->assertEquals( 6, $all_items_of_product_A_type[2]['quantity'] );
		$this->assertEquals( 6, $all_items_of_product_A_type[3]['quantity'] );
		$this->assertEquals( 8, $all_items_of_product_A_type[4]['quantity'] );

		$all_items_of_product_B_type = array_values(
			array_filter( $all_items, function ($item) use( $product_B ) {
				return $item['product_id'] === $product_B->get_id();
			})
		);

		/**
		 * Everything remains like before for the order item with quantity NOT exceeding the threshold
		 * e.g. each split will have quantity = 1, and we'll 19 splits.
		 */
		$this->assertEquals(19, count( $all_items_of_product_B_type ) );
		foreach ( $all_items_of_product_B_type as $item ) {
			$this->assertEquals( 1, $item['quantity'] );
		}
	}

	/**
	 * A helper to create a simple product.
	 *
	 * @param bool $virtual Whether the simple product is virtual or not.
	 * @return WC_Product_Simple
	 */
	private function create_simple_product( $virtual = false ): WC_Product_Simple {
		$product = WC_Helper_Product::create_simple_product();
		$product->set_virtual( $virtual );
		$product->save();
		return $product;
	}

	/**
	 * A helper to add a shipping label to an order.
	 *
	 * @param WC_Connect_Service_Settings_Store $settings_store Settings store that coordinates the shipping labels.
	 * @param WC_Order                          $order The order for the shipping label to be added to.
	 * @param array                             $product_ids A list of product IDs that the shipping label contains.
	 */
	private function add_shipping_label_to_order( $settings_store, $order, $product_ids = array() ) {
		$labels_meta = array(
			array(
				'label_id'    => 17,
				'product_ids' => $product_ids,
			),
		);
		$settings_store->add_labels_to_order( $order->get_id(), $labels_meta );
	}
}
