#!/usr/bin/env python3
# Copyright (c) 2023 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
"""
Tests for hardwaremap.py classes' methods
"""

import mock
import pytest
import sys

from pathlib import Path

from twisterlib.hardwaremap import(
    DUT,
    HardwareMap
)


@pytest.fixture
def mocked_hm():
    duts = [
        DUT(platform='p1', id=1, serial='s1', product='pr1', connected=True),
        DUT(platform='p2', id=2, serial='s2', product='pr2', connected=False),
        DUT(platform='p3', id=3, serial='s3', product='pr3', connected=True),
        DUT(platform='p4', id=4, serial='s4', product='pr4', connected=False),
        DUT(platform='p5', id=5, serial='s5', product='pr5', connected=True),
        DUT(platform='p6', id=6, serial='s6', product='pr6', connected=False),
        DUT(platform='p7', id=7, serial='s7', product='pr7', connected=True),
        DUT(platform='p8', id=8, serial='s8', product='pr8', connected=False)
    ]

    hm = HardwareMap(env=mock.Mock())
    hm.duts = duts
    hm.detected = duts[:5]

    return hm


TESTDATA_1 = [
    (
        {},
        {'baud': 115200, 'lock': mock.ANY, 'flash_timeout': 60},
        '<None (None) on None>'
    ),
    (
        {
            'id': 'dummy id',
            'serial': 'dummy serial',
            'serial_baud': 4400,
            'platform': 'dummy platform',
            'product': 'dummy product',
            'serial_pty': 'dummy serial pty',
            'connected': True,
            'runner_params': ['dummy', 'runner', 'params'],
            'pre_script': 'dummy pre script',
            'post_script': 'dummy post script',
            'post_flash_script': 'dummy post flash script',
            'runner': 'dummy runner',
            'flash_timeout': 30,
            'flash_with_test': True
        },
        {
            'lock': mock.ANY,
            'id': 'dummy id',
            'serial': 'dummy serial',
            'baud': 4400,
            'platform': 'dummy platform',
            'product': 'dummy product',
            'serial_pty': 'dummy serial pty',
            'connected': True,
            'runner_params': ['dummy', 'runner', 'params'],
            'pre_script': 'dummy pre script',
            'post_script': 'dummy post script',
            'post_flash_script': 'dummy post flash script',
            'runner': 'dummy runner',
            'flash_timeout': 30,
            'flash_with_test': True
        },
        '<dummy platform (dummy product) on dummy serial>'
    ),
]


@pytest.mark.parametrize(
    'kwargs, expected_dict, expected_repr',
    TESTDATA_1,
    ids=['no information', 'full information']
)
def test_dut(kwargs, expected_dict, expected_repr):
    d = DUT(**kwargs)

    assert d.available
    assert d.counter == 0

    d.available = False
    d.counter = 1

    assert not d.available
    assert d.counter == 1

    assert d.to_dict() == expected_dict
    assert d.__repr__() == expected_repr


TESTDATA_2 = [
    ('ghm.yaml', mock.ANY, mock.ANY, [], mock.ANY, mock.ANY, mock.ANY, 0,
     True, True, False, False, False, False, []),
    (None, False, 'hm.yaml', [], mock.ANY, mock.ANY, mock.ANY, 0,
     False, False, True, True, False, False, []),
    (None, True, 'hm.yaml', [], mock.ANY, mock.ANY, ['fix'], 1,
     False, False, True, False, False, True, ['p1', 'p3', 'p5', 'p7']),
    (None, True, 'hm.yaml', ['pX'], mock.ANY, mock.ANY, ['fix'], 1,
     False, False, True, False, False, True, ['pX']),
    (None, True, None, ['p'], 's', None, ['fix'], 1,
     False, False, False, False, True, True, ['p']),
    (None, True, None, ['p'], None, 'spty', ['fix'], 1,
     False, False, False, False, True, True, ['p']),
]


@pytest.mark.parametrize(
    'generate_hardware_map, device_testing, hardware_map, platform,' \
    ' device_serial, device_serial_pty, fixtures,' \
    ' return_code, expect_scan, expect_save, expect_load,' \
    ' expect_dump, expect_add_device, expect_fixtures, expected_platforms',
    TESTDATA_2,
    ids=['generate hardware map', 'existing hardware map',
         'device testing with hardware map, no platform',
         'device testing with hardware map with platform',
         'device testing with device serial',
         'device testing with device serial pty']
)
def test_hardwaremap_discover(
    caplog,
    mocked_hm,
    generate_hardware_map,
    device_testing,
    hardware_map,
    platform,
    device_serial,
    device_serial_pty,
    fixtures,
    return_code,
    expect_scan,
    expect_save,
    expect_load,
    expect_dump,
    expect_add_device,
    expect_fixtures,
    expected_platforms
):
    def mock_load(*args):
        mocked_hm.platform = platform

    mocked_hm.scan = mock.Mock()
    mocked_hm.save = mock.Mock()
    mocked_hm.load = mock.Mock(side_effect=mock_load)
    mocked_hm.dump = mock.Mock()
    mocked_hm.add_device = mock.Mock()

    mocked_hm.options.device_flash_with_test = True
    mocked_hm.options.device_flash_timeout = 15
    mocked_hm.options.pre_script = 'dummy pre script'
    mocked_hm.options.platform = platform
    mocked_hm.options.device_serial = device_serial
    mocked_hm.options.device_serial_pty = device_serial_pty
    mocked_hm.options.device_testing = device_testing
    mocked_hm.options.hardware_map = hardware_map
    mocked_hm.options.persistent_hardware_map = mock.Mock()
    mocked_hm.options.generate_hardware_map = generate_hardware_map
    mocked_hm.options.fixture = fixtures

    returncode = mocked_hm.discover()

    assert returncode == return_code

    if expect_scan:
        mocked_hm.scan.assert_called_once_with(
            persistent=mocked_hm.options.persistent_hardware_map
        )
    if expect_save:
        mocked_hm.save.assert_called_once_with(
            mocked_hm.options.generate_hardware_map
        )
    if expect_load:
        mocked_hm.load.assert_called_once_with(
            mocked_hm.options.hardware_map
        )
    if expect_dump:
        mocked_hm.dump.assert_called_once_with(
            connected_only=True
        )
    if expect_add_device:
        mocked_hm.add_device.assert_called_once()

    if expect_fixtures:
        assert all(
            [all(
                    [fixture in dut.fixtures for fixture in fixtures]
            ) for dut in mocked_hm.duts]
        )

    assert sorted(expected_platforms) == sorted(mocked_hm.options.platform)


def test_hardwaremap_summary(capfd, mocked_hm):
    selected_platforms = ['p0', 'p1', 'p6', 'p7']

    mocked_hm.summary(selected_platforms)

    expected = """
Hardware distribution summary:

| Board   |   ID |   Counter |
|---------|------|-----------|
| p1      |    1 |         0 |
| p7      |    7 |         0 |
"""

    out, err = capfd.readouterr()
    sys.stdout.write(out)
    sys.stderr.write(err)

    assert expected in out


TESTDATA_3 = [
    (True),
    (False)
]


@pytest.mark.parametrize(
    'is_pty',
    TESTDATA_3,
    ids=['pty', 'not pty']
)
def test_hardwaremap_add_device(is_pty):
    hm = HardwareMap(env=mock.Mock())

    serial = 'dummy'
    platform = 'p0'
    pre_script = 'dummy pre script'
    hm.add_device(serial, platform, pre_script, is_pty)

    assert len(hm.duts) == 1
    if is_pty:
        assert hm.duts[0].serial_pty == 'dummy' if is_pty else None
        assert hm.duts[0].serial is None
    else:
        assert hm.duts[0].serial_pty is None
        assert hm.duts[0].serial == 'dummy'


def test_hardwaremap_load():
    map_file = \
"""
- id: id0
  platform: p0
  product: pr0
  runner: r0
  flash_with_test: True
  flash_timeout: 15
  baud: 14400
  fixtures:
  - dummy fixture 1
  - dummy fixture 2
  connected: True
  serial: 'dummy'
- id: id1
  platform: p1
  product: pr1
  runner: r1
  connected: True
  serial_pty: 'dummy'
- id: id2
  platform: p2
  product: pr2
  runner: r2
  connected: True
"""
    map_filename = 'map-file.yaml'

    builtin_open = open

    def mock_open(*args, **kwargs):
        if args[0] == map_filename:
            return mock.mock_open(read_data=map_file)(*args, **kwargs)
        return builtin_open(*args, **kwargs)

    hm = HardwareMap(env=mock.Mock())
    hm.options.device_flash_timeout = 30
    hm.options.device_flash_with_test = False

    with mock.patch('builtins.open', mock_open):
        hm.load(map_filename)

    expected = {
        'id0': {
            'platform': 'p0',
            'product': 'pr0',
            'runner': 'r0',
            'flash_timeout': 15,
            'flash_with_test': True,
            'baud': 14400,
            'fixtures': ['dummy fixture 1', 'dummy fixture 2'],
            'connected': True,
            'serial': 'dummy',
            'serial_pty': None,
        },
        'id1': {
            'platform': 'p1',
            'product': 'pr1',
            'runner': 'r1',
            'flash_timeout': 30,
            'flash_with_test': False,
            'baud': 115200,
            'fixtures': [],
            'connected': True,
            'serial': None,
            'serial_pty': 'dummy',
        },
    }

    for dut in hm.duts:
        assert dut.id in expected
        assert all([getattr(dut, k) == v for k, v in expected[dut.id].items()])


TESTDATA_4 = [
    (
        True,
        'Linux',
        ['<p1 (pr1) on s1>', '<p2 (pr2) on s2>', '<p3 (pr3) on s3>',
         '<p4 (pr4) on s4>', '<p5 (pr5) on s5>',
         '<unknown (TI product) on /dev/serial/by-id/basic-file1>',
         '<unknown (product123) on dummy device>',
         '<unknown (unknown) on /dev/serial/by-id/basic-file2-link>']
    ),
    (
        True,
        'nt',
        ['<p1 (pr1) on s1>', '<p2 (pr2) on s2>', '<p3 (pr3) on s3>',
         '<p4 (pr4) on s4>', '<p5 (pr5) on s5>',
         '<unknown (TI product) on /dev/serial/by-id/basic-file1>',
         '<unknown (product123) on dummy device>',
         '<unknown (unknown) on /dev/serial/by-id/basic-file2>']
    ),
    (
        False,
        'Linux',
        ['<p1 (pr1) on s1>', '<p2 (pr2) on s2>', '<p3 (pr3) on s3>',
         '<p4 (pr4) on s4>', '<p5 (pr5) on s5>',
         '<unknown (TI product) on /dev/serial/by-id/basic-file1>',
         '<unknown (product123) on dummy device>',
         '<unknown (unknown) on /dev/serial/by-id/basic-file2>']
    )
]


@pytest.mark.parametrize(
    'persistent, system, expected_reprs',
    TESTDATA_4,
    ids=['linux persistent map', 'no map (not linux)', 'no map (nonpersistent)']
)
def test_hardwaremap_scan(
    caplog,
    mocked_hm,
    persistent,
    system,
    expected_reprs
):
    def mock_resolve(path):
        if str(path).endswith('-link'):
            return Path(str(path)[:-5])
        return path

    def mock_iterdir(path):
        return [
            Path(path / 'basic-file1'),
            Path(path / 'basic-file2-link')
        ]

    mocked_hm.manufacturer = ['dummy manufacturer', 'Texas Instruments']
    mocked_hm.runner_mapping = {
        'dummy runner': ['product[0-9]+',],
        'other runner': ['other TI product', 'TI product']
    }

    comports_mock = [
        mock.Mock(
            manufacturer='wrong manufacturer',
            location='wrong location',
            serial_number='wrong number',
            product='wrong product',
            device='wrong device'
        ),
        mock.Mock(
            manufacturer='dummy manufacturer',
            location='dummy location',
            serial_number='dummy number',
            product=None,
            device='/dev/serial/by-id/basic-file2'
        ),
        mock.Mock(
            manufacturer='dummy manufacturer',
            location='dummy location',
            serial_number='dummy number',
            product='product123',
            device='dummy device'
        ),
        mock.Mock(
            manufacturer='Texas Instruments',
            location='serial1',
            serial_number='TI1',
            product='TI product',
            device='TI device1'
        ),
        mock.Mock(
            manufacturer='Texas Instruments',
            location='serial0',
            serial_number='TI0',
            product='TI product',
            device='/dev/serial/by-id/basic-file1'
        ),
    ]

    with mock.patch('platform.system', return_value=system), \
         mock.patch('serial.tools.list_ports.comports',
                    return_value=comports_mock), \
         mock.patch('twisterlib.hardwaremap.Path.resolve',
                    autospec=True, side_effect=mock_resolve), \
         mock.patch('twisterlib.hardwaremap.Path.iterdir',
                    autospec=True, side_effect=mock_iterdir):
        mocked_hm.scan(persistent)

    assert sorted([d.__repr__() for d in mocked_hm.detected]) == \
           sorted(expected_reprs)

    assert 'Scanning connected hardware...' in caplog.text
    assert 'Unsupported device (wrong manufacturer): %s' % comports_mock[0] \
           in caplog.text


TESTDATA_5 = [
    (
        None,
        [{
            'platform': 'p1',
            'id': 1,
            'runner': mock.ANY,
            'serial': 's1',
            'product': 'pr1',
            'connected': True
        },
        {
            'platform': 'p2',
            'id': 2,
            'runner': mock.ANY,
            'serial': 's2',
            'product': 'pr2',
            'connected': False
        },
        {
            'platform': 'p3',
            'id': 3,
            'runner': mock.ANY,
            'serial': 's3',
            'product': 'pr3',
            'connected': True
        },
        {
            'platform': 'p4',
            'id': 4,
            'runner': mock.ANY,
            'serial': 's4',
            'product': 'pr4',
            'connected': False
        },
        {
            'platform': 'p5',
            'id': 5,
            'runner': mock.ANY,
            'serial': 's5',
            'product': 'pr5',
            'connected': True
        }]
    ),
    (
        '',
        [{
            'serial': 's1',
            'baud': 115200,
            'platform': 'p1',
            'connected': True,
            'id': 1,
            'product': 'pr1',
            'lock': mock.ANY,
            'flash_timeout': 60
        },
        {
            'serial': 's2',
            'baud': 115200,
            'platform': 'p2',
            'id': 2,
            'product': 'pr2',
            'lock': mock.ANY,
            'flash_timeout': 60
        },
        {
            'serial': 's3',
            'baud': 115200,
            'platform': 'p3',
            'connected': True,
            'id': 3,
            'product': 'pr3',
            'lock': mock.ANY,
            'flash_timeout': 60
        },
        {
            'serial': 's4',
            'baud': 115200,
            'platform': 'p4',
            'id': 4,
            'product': 'pr4',
            'lock': mock.ANY,
            'flash_timeout': 60
        },
        {
            'serial': 's5',
            'baud': 115200,
            'platform': 'p5',
            'connected': True,
            'id': 5,
            'product': 'pr5',
            'lock': mock.ANY,
            'flash_timeout': 60
        }]
    ),
    (
"""
- id: 4
  platform: p4
  product: pr4
  connected: True
  serial: s4
- id: 0
  platform: p0
  product: pr0
  connected: True
  serial: s0
- id: 10
  platform: p10
  product: pr10
  connected: False
  serial: s10
- id: 5
  platform: p5-5
  product: pr5-5
  connected: True
  serial: s5-5
""",
        [{
            'id': 0,
            'platform': 'p0',
            'product': 'pr0',
            'connected': False,
            'serial': None
        },
        {
            'id': 4,
            'platform': 'p4',
            'product': 'pr4',
            'connected': True,
            'serial': 's4'
        },
        {
            'id': 5,
            'platform': 'p5-5',
            'product': 'pr5-5',
            'connected': False,
            'serial': None
        },
        {
            'id': 10,
            'platform': 'p10',
            'product': 'pr10',
            'connected': False,
            'serial': None
        },
        {
            'serial': 's1',
            'baud': 115200,
            'platform': 'p1',
            'connected': True,
            'id': 1,
            'product': 'pr1',
            'lock': mock.ANY,
            'flash_timeout': 60
        },
        {
            'serial': 's2',
            'baud': 115200,
            'platform': 'p2',
            'id': 2,
            'product': 'pr2',
            'lock': mock.ANY,
            'flash_timeout': 60
        },
        {
            'serial': 's3',
            'baud': 115200,
            'platform': 'p3',
            'connected': True,
            'id': 3,
            'product': 'pr3',
            'lock': mock.ANY,
            'flash_timeout': 60
        },
        {
            'serial': 's5',
            'baud': 115200,
            'platform': 'p5',
            'connected': True,
            'id': 5,
            'product': 'pr5',
            'lock': mock.ANY,
            'flash_timeout': 60
        }]
    ),
]


@pytest.mark.parametrize(
    'hwm, expected_dump',
    TESTDATA_5,
    ids=['no map', 'empty map', 'map exists']
)
def test_hardwaremap_save(mocked_hm, hwm, expected_dump):
    read_mock = mock.mock_open(read_data=hwm)
    write_mock = mock.mock_open()

    def mock_open(filename, mode):
        if mode == 'r':
            return read_mock()
        elif mode == 'w':
            return write_mock()


    mocked_hm.load = mock.Mock()
    mocked_hm.dump = mock.Mock()

    open_mock = mock.Mock(side_effect=mock_open)
    dump_mock = mock.Mock()

    with mock.patch('os.path.exists', return_value=hwm is not None), \
         mock.patch('builtins.open', open_mock), \
         mock.patch('twisterlib.hardwaremap.yaml.dump', dump_mock):
        mocked_hm.save('hwm.yaml')

    dump_mock.assert_called_once_with(expected_dump, mock.ANY, Dumper=mock.ANY,
                                      default_flow_style=mock.ANY)


TESTDATA_6 = [
    (
        ['p1', 'p3', 'p5', 'p7'],
        [],
        True,
        True,
"""
| Platform   |   ID | Serial device   |
|------------|------|-----------------|
| p1         |    1 | s1              |
| p3         |    3 | s3              |
| p5         |    5 | s5              |
"""
    ),
    (
        [],
        ['?', '??', '???'],
        False,
        False,
"""
| ?   |   ?? | ???   |
|-----|------|-------|
| p1  |    1 | s1    |
| p2  |    2 | s2    |
| p3  |    3 | s3    |
| p4  |    4 | s4    |
| p5  |    5 | s5    |
| p6  |    6 | s6    |
| p7  |    7 | s7    |
| p8  |    8 | s8    |
"""
    ),
]


@pytest.mark.parametrize(
    'filtered, header, connected_only, detected, expected_out',
    TESTDATA_6,
    ids=['detected no header', 'all with header']
)
def test_hardwaremap_dump(
    capfd,
    mocked_hm,
    filtered,
    header,
    connected_only,
    detected,
    expected_out
):
    mocked_hm.dump(filtered, header, connected_only, detected)

    out, err = capfd.readouterr()
    sys.stdout.write(out)
    sys.stderr.write(err)

    assert out.strip() == expected_out.strip()
