/*
 * Unit tests for the avi splitter functions
 *
 * Copyright (C) 2007 Google (Lei Zhang)
 * Copyright (C) 2008 Google (Maarten Lankhorst)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#define COBJMACROS

#include "wine/test.h"
#include "dshow.h"
#include "tlhelp32.h"

static HANDLE (WINAPI *pCreateToolhelp32Snapshot)(DWORD, DWORD);
static BOOL (WINAPI *pThread32First)(HANDLE, LPTHREADENTRY32);
static BOOL (WINAPI *pThread32Next)(HANDLE, LPTHREADENTRY32);

static IUnknown *pAviSplitter = NULL;

static int count_threads(void)
{
    THREADENTRY32 te;
    int threads;
    HANDLE h;

    h = pCreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    te.dwSize = sizeof(te);

    if (h == INVALID_HANDLE_VALUE)
        return -1;

    pThread32First(h, &te);
    if (te.th32OwnerProcessID == GetCurrentProcessId())
        threads = 1;
    else
        threads = 0;

    while (pThread32Next(h, &te))
        if (te.th32OwnerProcessID == GetCurrentProcessId())
            ++threads;

    CloseHandle(h);
    return threads;
}

static BOOL create_avisplitter(void)
{
    HRESULT hr;

    hr = CoCreateInstance(&CLSID_AviSplitter, NULL, CLSCTX_INPROC_SERVER,
                          &IID_IUnknown, (LPVOID*)&pAviSplitter);
    return (hr == S_OK && pAviSplitter != NULL);
}

static void release_avisplitter(void)
{
    HRESULT hr;

    Sleep(1000);
    hr = IUnknown_Release(pAviSplitter);

    /* Looks like wine has a reference leak somewhere on test_threads tests,
     * it passes in windows
     */
    ok(hr == 0, "IUnknown_Release failed with %d\n", (INT)hr);

    while (hr > 0)
        hr = IUnknown_Release(pAviSplitter);
    pAviSplitter = NULL;
}

static void test_query_interface(void)
{
    HRESULT hr;
    ULONG ref;
    IUnknown *iface= NULL;

#define TEST_INTERFACE(riid,expected) do { \
    hr = IUnknown_QueryInterface(pAviSplitter, &riid, (void**)&iface); \
    ok( hr == expected, #riid" should %s got %08X\n", expected==S_OK ? "exist" : "not be present", GetLastError() ); \
    if (hr == S_OK) { \
        ref = IUnknown_Release(iface); \
        ok(ref == 1, "Reference is %u, expected 1\n", ref); \
    } \
    iface = NULL; \
    } while(0)

    TEST_INTERFACE(IID_IBaseFilter,S_OK);
    TEST_INTERFACE(IID_IMediaSeeking,E_NOINTERFACE);
    TEST_INTERFACE(IID_IKsPropertySet,E_NOINTERFACE);
    TEST_INTERFACE(IID_IMediaPosition,E_NOINTERFACE);
    TEST_INTERFACE(IID_IQualityControl,E_NOINTERFACE);
    TEST_INTERFACE(IID_IQualProp,E_NOINTERFACE);
#undef TEST_INTERFACE
}

static void test_pin(IPin *pin)
{
    IMemInputPin *mpin = NULL;

    IPin_QueryInterface(pin, &IID_IMemInputPin, (void **)&mpin);

    ok(mpin == NULL, "IMemInputPin found!\n");
    if (mpin)
        IMemInputPin_Release(mpin);
    /* TODO */
}

static void test_basefilter(void)
{
    IEnumPins *pin_enum = NULL;
    IBaseFilter *base = NULL;
    IPin *pins[2];
    ULONG ref;
    HRESULT hr;

    IUnknown_QueryInterface(pAviSplitter, &IID_IBaseFilter, (void **)&base);
    if (base == NULL)
    {
        /* test_query_interface handles this case */
        skip("No IBaseFilter\n");
        return;
    }

    hr = IBaseFilter_EnumPins(base, NULL);
    ok(hr == E_POINTER, "hr = %08x and not E_POINTER\n", hr);

    hr= IBaseFilter_EnumPins(base, &pin_enum);
    ok(hr == S_OK, "hr = %08x and not S_OK\n", hr);

    hr = IEnumPins_Next(pin_enum, 1, NULL, NULL);
    ok(hr == E_POINTER, "hr = %08x and not E_POINTER\n", hr);

    hr = IEnumPins_Next(pin_enum, 2, pins, NULL);
    ok(hr == E_INVALIDARG, "hr = %08x and not E_INVALIDARG\n", hr);

    pins[0] = (void *)0xdead;
    pins[1] = (void *)0xdeed;

    hr = IEnumPins_Next(pin_enum, 2, pins, &ref);
    ok(hr == S_FALSE, "hr = %08x instead of S_FALSE\n", hr);
    ok(pins[0] != (void *)0xdead && pins[0] != NULL,
        "pins[0] = %p\n", pins[0]);
    if (pins[0] != (void *)0xdead && pins[0] != NULL)
    {
        test_pin(pins[0]);
        IPin_Release(pins[0]);
    }

    ok(pins[1] == (void *)0xdeed, "pins[1] = %p\n", pins[1]);

    ref = IEnumPins_Release(pin_enum);
    ok(ref == 0, "ref is %u and not 0!\n", ref);

    IBaseFilter_Release(base);
}

static void test_filesourcefilter(void)
{
    static const WCHAR prefix[] = {'w','i','n',0};
    static const struct
    {
        const char *label;
        const char *data;
        DWORD size;
        const GUID *subtype;
    }
    tests[] =
    {
        {
            "AVI",
            "\x52\x49\x46\x46xxxx\x41\x56\x49\x20",
            12,
            &MEDIASUBTYPE_Avi,
        },
        {
            "MPEG1 System",
            "\x00\x00\x01\xBA\x21\x00\x01\x00\x01\x80\x00\x01\x00\x00\x01\xBB",
            16,
            &MEDIASUBTYPE_MPEG1System,
        },
        {
            "MPEG1 Video",
            "\x00\x00\x01\xB3",
            4,
            &MEDIASUBTYPE_MPEG1Video,
        },
        {
            "MPEG1 Audio",
            "\xFF\xE0",
            2,
            &MEDIASUBTYPE_MPEG1Audio,
        },
        {
            "MPEG2 Program",
            "\x00\x00\x01\xBA\x40",
            5,
            &MEDIASUBTYPE_MPEG2_PROGRAM,
        },
        {
            "WAVE",
            "\x52\x49\x46\x46xxxx\x57\x41\x56\x45",
            12,
            &MEDIASUBTYPE_WAVE,
        },
        {
            "unknown format",
            "Hello World",
            11,
            NULL, /* FIXME: should be &MEDIASUBTYPE_NULL */
        },
    };
    WCHAR path[MAX_PATH], temp[MAX_PATH];
    IFileSourceFilter *filesource;
    DWORD ret, written;
    IBaseFilter *base;
    AM_MEDIA_TYPE mt;
    OLECHAR *olepath;
    BOOL success;
    HANDLE file;
    HRESULT hr;
    int i;

    ret = GetTempPathW(MAX_PATH, temp);
    ok(ret, "GetTempPathW failed with error %u\n", GetLastError());
    ret = GetTempFileNameW(temp, prefix, 0, path);
    ok(ret, "GetTempFileNameW failed with error %u\n", GetLastError());

    for (i = 0; i < sizeof(tests)/sizeof(tests[0]); i++)
    {
        trace("Running test for %s\n", tests[i].label);

        file = CreateFileW(path, GENERIC_READ | GENERIC_WRITE, 0, NULL,
                           CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        ok(file != INVALID_HANDLE_VALUE, "CreateFileW failed with error %u\n", GetLastError());
        success = WriteFile(file, tests[i].data, tests[i].size, &written, NULL);
        ok(success, "WriteFile failed with error %u\n", GetLastError());
        ok(written == tests[i].size, "could not write test data\n");
        CloseHandle(file);

        hr = CoCreateInstance(&CLSID_AsyncReader, NULL, CLSCTX_INPROC_SERVER,
                              &IID_IBaseFilter, (void **)&base);
        ok(hr == S_OK, "CoCreateInstance failed with %08x\n", hr);
        hr = IBaseFilter_QueryInterface(base, &IID_IFileSourceFilter, (void **)&filesource);
        ok(hr == S_OK, "IBaseFilter_QueryInterface failed with %08x\n", hr);

        olepath = (void *)0xdeadbeef;
        hr = IFileSourceFilter_GetCurFile(filesource, &olepath, NULL);
        ok(hr == S_OK, "expected S_OK, got %08x\n", hr);
        ok(olepath == NULL, "expected NULL, got %p\n", olepath);

        hr = IFileSourceFilter_Load(filesource, NULL, NULL);
        ok(hr == E_POINTER, "expected E_POINTER, got %08x\n", hr);

        hr = IFileSourceFilter_Load(filesource, path, NULL);
        ok(hr == S_OK, "IFileSourceFilter_Load failed with %08x\n", hr);

        hr = IFileSourceFilter_GetCurFile(filesource, NULL, &mt);
        ok(hr == E_POINTER, "expected E_POINTER, got %08x\n", hr);

        olepath = NULL;
        hr = IFileSourceFilter_GetCurFile(filesource, &olepath, NULL);
        ok(hr == S_OK, "expected S_OK, got %08x\n", hr);
        CoTaskMemFree(olepath);

        olepath = NULL;
        memset(&mt, 0x11, sizeof(mt));
        hr = IFileSourceFilter_GetCurFile(filesource, &olepath, &mt);
        ok(hr == S_OK, "expected S_OK, got %08x\n", hr);
        ok(!lstrcmpW(olepath, path),
           "expected %s, got %s\n", wine_dbgstr_w(path), wine_dbgstr_w(olepath));
        if (tests[i].subtype)
        {
            ok(IsEqualGUID(&mt.majortype, &MEDIATYPE_Stream),
               "expected MEDIATYPE_Stream, got %s\n", wine_dbgstr_guid(&mt.majortype));
            ok(IsEqualGUID(&mt.subtype, tests[i].subtype),
               "expected %s, got %s\n", wine_dbgstr_guid(tests[i].subtype), wine_dbgstr_guid(&mt.subtype));
        }
        CoTaskMemFree(olepath);

        IFileSourceFilter_Release(filesource);
        IBaseFilter_Release(base);

        success = DeleteFileW(path);
        ok(success, "DeleteFileW failed with error %u\n", GetLastError());
    }
}

static const WCHAR wfile[] = {'t','e','s','t','.','a','v','i',0};
static const char afile[] = "test.avi";

/* This test doesn't use the quartz filtergraph because it makes it impossible
 * to be certain that a thread is really one owned by the avi splitter.
 * A lot of the decoder filters will also have their own thread, and Windows'
 * filtergraph has a separate thread for start/stop/seeking requests.
 * By avoiding the filtergraph altogether and connecting streams directly to
 * the null renderer I am sure that this is not the case here.
 */
static void test_threads(void)
{
    IFileSourceFilter *pfile = NULL;
    IBaseFilter *preader = NULL, *pavi = NULL;
    IEnumPins *enumpins = NULL;
    IPin *filepin = NULL, *avipin = NULL;
    HRESULT hr;
    int baselevel, curlevel, expected;
    HANDLE file = NULL;
    PIN_DIRECTION dir = PINDIR_OUTPUT;
    char buffer[13];
    DWORD readbytes;
    FILTER_STATE state;

    /* We need another way of counting threads on NT4. Skip these tests (for now) */
    if (!pCreateToolhelp32Snapshot || !pThread32First || !pThread32Next)
    {
        win_skip("Needed thread functions are not available (NT4)\n");
        return;
    }

    /* Before doing anything (the thread count at the start differs per OS) */
    baselevel = count_threads();

    file = CreateFileW(wfile, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE,
        NULL, OPEN_EXISTING, 0, NULL);
    if (file == INVALID_HANDLE_VALUE)
    {
        skip("Could not read test file \"%s\", skipping test\n", afile);
        return;
    }

    memset(buffer, 0, 13);
    readbytes = 12;
    ReadFile(file, buffer, readbytes, &readbytes, NULL);
    CloseHandle(file);
    if (strncmp(buffer, "RIFF", 4) || strcmp(buffer + 8, "AVI "))
    {
        skip("%s is not an avi riff file, not doing the avi splitter test\n",
            afile);
        return;
    }

    hr = IUnknown_QueryInterface(pAviSplitter, &IID_IFileSourceFilter,
        (void **)&pfile);
    ok(hr == E_NOINTERFACE,
        "Avi splitter returns unexpected error: %08x\n", hr);
    if (pfile)
        IFileSourceFilter_Release(pfile);
    pfile = NULL;

    hr = CoCreateInstance(&CLSID_AsyncReader, NULL, CLSCTX_INPROC_SERVER,
        &IID_IBaseFilter, (LPVOID*)&preader);
    ok(hr == S_OK, "Could not create asynchronous reader: %08x\n", hr);
    if (hr != S_OK)
        goto fail;

    hr = IBaseFilter_QueryInterface(preader, &IID_IFileSourceFilter,
        (void**)&pfile);
    ok(hr == S_OK, "Could not get IFileSourceFilter: %08x\n", hr);
    if (hr != S_OK)
        goto fail;

    hr = IUnknown_QueryInterface(pAviSplitter, &IID_IBaseFilter,
        (void**)&pavi);
    ok(hr == S_OK, "Could not get base filter: %08x\n", hr);
    if (hr != S_OK)
        goto fail;

    hr = IFileSourceFilter_Load(pfile, wfile, NULL);
    if (hr != S_OK)
    {
        trace("Could not load file\n");
        goto fail;
    }

    hr = IBaseFilter_EnumPins(preader, &enumpins);
    ok(hr == S_OK, "No enumpins: %08x\n", hr);
    if (hr != S_OK)
        goto fail;

    hr = IEnumPins_Next(enumpins, 1, &filepin, NULL);
    ok(hr == S_OK, "No pin: %08x\n", hr);
    if (hr != S_OK)
        goto fail;

    IEnumPins_Release(enumpins);
    enumpins = NULL;

    hr = IBaseFilter_EnumPins(pavi, &enumpins);
    ok(hr == S_OK, "No enumpins: %08x\n", hr);
    if (hr != S_OK)
        goto fail;

    hr = IEnumPins_Next(enumpins, 1, &avipin, NULL);
    ok(hr == S_OK, "No pin: %08x\n", hr);
    if (hr != S_OK)
        goto fail;

    curlevel = count_threads();
    ok(curlevel == baselevel,
        "The thread count should be %d not %d\n", baselevel, curlevel);

    hr = IPin_Connect(filepin, avipin, NULL);
    ok(hr == S_OK, "Could not connect: %08x\n", hr);
    if (hr != S_OK)
        goto fail;

    expected = 1 + baselevel;
    curlevel = count_threads();
    ok(curlevel == expected,
        "The thread count should be %d not %d\n", expected, curlevel);

    IPin_Release(avipin);
    avipin = NULL;

    IEnumPins_Reset(enumpins);

    /* Windows puts the pins in the order: Outputpins - Inputpin,
     * wine does the reverse, just don't test it for now
     * Hate to admit it, but windows way makes more sense
     */
    while (IEnumPins_Next(enumpins, 1, &avipin, NULL) == S_OK)
    {
        IPin_QueryDirection(avipin, &dir);
        if (dir == PINDIR_OUTPUT)
        {
            /* Well, connect it to a null renderer! */
            IBaseFilter *pnull = NULL;
            IEnumPins *nullenum = NULL;
            IPin *nullpin = NULL;

            hr = CoCreateInstance(&CLSID_NullRenderer, NULL,
                CLSCTX_INPROC_SERVER, &IID_IBaseFilter, (LPVOID*)&pnull);
            ok(hr == S_OK, "Could not create null renderer: %08x\n", hr);
            if (hr != S_OK)
                break;

            IBaseFilter_EnumPins(pnull, &nullenum);
            IEnumPins_Next(nullenum, 1, &nullpin, NULL);
            IEnumPins_Release(nullenum);
            IPin_QueryDirection(nullpin, &dir);

            hr = IPin_Connect(avipin, nullpin, NULL);
            ok(hr == S_OK, "Failed to connect output pin: %08x\n", hr);
            IPin_Release(nullpin);
            if (hr != S_OK)
            {
                IBaseFilter_Release(pnull);
                break;
            }
            IBaseFilter_Run(pnull, 0);
            ++expected;
        }

        IPin_Release(avipin);
        avipin = NULL;
    }

    if (avipin)
        IPin_Release(avipin);
    avipin = NULL;

    if (hr != S_OK)
        goto fail2;
    /* At this point there is a minimalistic connected avi splitter that can
     * be used for all sorts of source filter tests. However that still needs
     * to be written at a later time.
     *
     * Interesting tests:
     * - Can you disconnect an output pin while running?
     *   Expecting: Yes
     * - Can you disconnect the pullpin while running?
     *   Expecting: No
     * - Is the reference count incremented during playback or when connected?
     *   Does this happen once for every output pin? Or is there something else
     *   going on.
     *   Expecting: You tell me
     */

    IBaseFilter_Run(preader, 0);
    IBaseFilter_Run(pavi, 0);
    IBaseFilter_GetState(pavi, INFINITE, &state);

    curlevel = count_threads();
    ok(curlevel == expected,
        "The thread count should be %d not %d\n", expected, curlevel);

    IBaseFilter_Pause(pavi);
    IBaseFilter_Pause(preader);
    IBaseFilter_Stop(pavi);
    IBaseFilter_Stop(preader);
    IBaseFilter_GetState(pavi, INFINITE, &state);
    IBaseFilter_GetState(preader, INFINITE, &state);

fail2:
    IEnumPins_Reset(enumpins);
    while (IEnumPins_Next(enumpins, 1, &avipin, NULL) == S_OK)
    {
        IPin *to = NULL;

        IPin_QueryDirection(avipin, &dir);
        IPin_ConnectedTo(avipin, &to);
        if (to)
        {
            IPin_Release(to);

            if (dir == PINDIR_OUTPUT)
            {
                PIN_INFO info;
                IPin_QueryPinInfo(to, &info);

                /* Release twice: Once normal, second from the
                 * previous while loop
                 */
                IBaseFilter_Stop(info.pFilter);
                IPin_Disconnect(to);
                IPin_Disconnect(avipin);
                IBaseFilter_Release(info.pFilter);
                IBaseFilter_Release(info.pFilter);
            }
            else
            {
                IPin_Disconnect(to);
                IPin_Disconnect(avipin);
            }
        }
        IPin_Release(avipin);
        avipin = NULL;
    }

fail:
    if (hr != S_OK)
        skip("Prerequisites not matched, skipping remainder of test\n");
    if (enumpins)
        IEnumPins_Release(enumpins);

    if (avipin)
        IPin_Release(avipin);
    if (filepin)
    {
        IPin *to = NULL;

        IPin_ConnectedTo(filepin, &to);
        if (to)
        {
            IPin_Disconnect(filepin);
            IPin_Disconnect(to);
        }
        IPin_Release(filepin);
    }

    if (preader)
        IBaseFilter_Release(preader);
    if (pavi)
        IBaseFilter_Release(pavi);
    if (pfile)
        IFileSourceFilter_Release(pfile);

    curlevel = count_threads();
    todo_wine
    ok(curlevel == baselevel,
        "The thread count should be %d not %d\n", baselevel, curlevel);
}

START_TEST(avisplitter)
{
    HMODULE kernel32 = GetModuleHandleA("kernel32.dll");

    pCreateToolhelp32Snapshot = (void*)GetProcAddress(kernel32, "CreateToolhelp32Snapshot");
    pThread32First = (void*)GetProcAddress(kernel32, "Thread32First");
    pThread32Next = (void*)GetProcAddress(kernel32, "Thread32Next");

    CoInitialize(NULL);

    if (!create_avisplitter())
    {
        skip("Could not create avisplitter\n");
        return;
    }

    test_query_interface();
    test_basefilter();
    test_filesourcefilter();
    test_threads();

    release_avisplitter();

    CoUninitialize();
}
